بناء مستكشف GitHub Repo باستخدام React و Elasticsearch

يعد Elasticsearch أحد أكثر محركات البحث عن النص الكامل شيوعًا والذي يسمح لك بالبحث عن كميات هائلة من البيانات بسرعة ، بينما يمكن القول إن React هي أفضل مكتبة لبناء واجهات المستخدم. خلال الأشهر القليلة الماضية ، شاركت في تأليف مكتبة مفتوحة المصدر ، ReactiveSearch ، والتي توفر مكونات React لـ Elasticsearch وتبسط عملية إنشاء واجهة مستخدم للبحث (UI).

هذا هو التطبيق الذي سأبنيه في هذه القصة:

فكرة موجزة عن Elasticsearch

Elasticsearch هي قاعدة بيانات NoSQL يمكنها البحث في كميات كبيرة من البيانات في وقت قصير. يقوم بإجراء بحث عن النص الكامل للبيانات المخزنة في شكل مستندات (مثل الكائنات) عن طريق فحص جميع الكلمات في كل مستند.

إليك ما يقوله مستندات Elasticsearch:

Elasticsearch هو محرك بحث وتحليلات مفتوح المصدر ومفتوح المصدر بدرجة عالية. يتيح لك تخزين كميات كبيرة من البيانات والبحث عنها وتحليلها بسرعة وفي الوقت الفعلي تقريبًا.

حتى إذا لم تستخدم Elasticsearch مطلقًا قبل أن تتمكن من متابعة هذه القصة وبناء بحث Elasticsearch خاص بك باستخدام React and ReactiveSearch. ؟

ما هو البحث التفاعلي؟

ReactiveSearch هي مكتبة مكونات React UI لـ Elasticsearch. من أجل البحث عن البيانات في Elasticsearch ، تحتاج إلى كتابة استفسارات . ستحتاج بعد ذلك إلى تنسيق بيانات JSON وعرضها في واجهة المستخدم الخاصة بك. يبسط ReactiveSearch العملية برمتها حيث لا داعي للقلق بشأن كتابة هذه الاستعلامات. هذا يجعل من السهل التركيز على إنشاء واجهة المستخدم.

فيما يلي مثال يُنشئ واجهة مستخدم لمربع البحث مع اقتراحات خاصة بفئة:

كان من المحتمل أن يأخذنا هذا أكثر من 100 سطر بدون المكتبة ، ومعرفة Elasticsearch Query DSL لإنشاء الاستعلام.

في هذا المنشور ، سأستخدم مكونات مختلفة من المكتبة لبناء واجهة المستخدم النهائية.

يجب أن تجرب التطبيق النهائي قبل أن نتعمق. إليك رابط CodeSandbox لنفسه.

إعداد الأمور

قبل أن نبدأ في إنشاء واجهة المستخدم ، سنحتاج إلى مجموعة البيانات التي تحتوي على مستودعات GitHub في Elasticsearch. يعمل ReactiveSearch مع أي فهرس Elasticsearch ويمكنك استخدامه بسهولة مع مجموعة البيانات الخاصة بك.

للإيجاز ، يمكنك استخدام مجموعة البيانات الخاصة بي أو استنساخها بنفسك عن طريق اتباع هذا الرابط والنقر على زر استنساخ هذا التطبيق . سيتيح لك ذلك عمل نسخة من مجموعة البيانات كتطبيق خاص بك.

بعد إدخال اسم التطبيق ، يجب أن تبدأ عملية الاستنساخ في استيراد 26K + repos إلى حسابك.

يتم تنظيم جميع اتفاقيات إعادة الشراء بالتنسيق التالي:

{ "name": "freeCodeCamp", "owner": "freeCodeCamp", "fullname": "freeCodeCamp~freeCodeCamp", "description": "The //freeCodeCamp.org open source codebase and curriculum. Learn to code and help nonprofits.", "avatar": "//avatars0.githubusercontent.com/u/9892522?v=4", "url": "//github.com/freeCodeCamp/freeCodeCamp", "pushed": "2017-12-24T05:44:03Z", "created": "2014-12-24T17:49:19Z", "size": 31474, "stars": 291526, "forks": 13211, "topics": [ "careers", "certification", "community", "curriculum", "d3", "education", "javascript", "learn-to-code", "math", "nodejs", "nonprofits", "programming", "react", "teachers" ], "language": "JavaScript", "watchers": 8462 }
  • سنستخدم تطبيق create-react-app لإعداد المشروع. يمكنك تثبيت تطبيق create-react-app عن طريق تشغيل الأمر التالي في جهازك الطرفي:
npm install -g create-react-app
  • بعد تثبيته ، يمكنك إنشاء مشروع جديد عن طريق تشغيل:
create-react-app gitxplore
  • بعد إعداد المشروع ، يمكنك التغيير إلى دليل المشروع وتثبيت تبعية ReactiveSearch:
cd gitxplore npm install @appbaseio/reactivesearch
  • يمكنك أيضًا إضافة Fontawesome CDN ، والذي سنستخدمه لبعض الرموز ، عن طريق إدراج الأسطر التالية /public/index.htmlقبل انتهاء العلامة:

الغوص في الكود

سأتبع بنية دليل بسيطة للتطبيق. فيما يلي الملفات المهمة:

src ├── App.css // App styles ├── App.js // App container ├── components │ ├── Header.js // Header component │ ├── Results.js // Results component │ ├── SearchFilters.js // Filters component │ └── Topic.js // rendered by Results ├── index.css // styles ├── index.js // ReactDOM render └── theme.js // colors and fonts public └── index.html

إليك رابط الريبو النهائي إذا كنت ترغب في الإشارة إلى أي شيء في أي وقت.

1. إضافة الأنماط

لقد كتبت أنماطًا سريعة الاستجابة للتطبيق يمكنك نسخها إلى تطبيقك. ما عليك سوى تشغيل محرر النصوص المفضل لديك ونسخ الأنماط /src/index.cssمن هنا ومن /src/App.cssهنا على التوالي.

الآن ، قم بإنشاء ملف /src/theme.jsحيث سنضيف الألوان والخطوط لتطبيقنا:

const theme = { typography: { fontFamily: 'Raleway, Helvetica, sans-serif', }, colors: { primaryColor: '#008000', titleColor: 'white' }, secondaryColor: 'mediumseagreen', }; export default theme;

2. إضافة مكون ReactiveSearch الأول

يتم التفاف جميع مكونات ReactiveSearch حول مكون حاوية ReactiveBase الذي يوفر بيانات من Elasticsearch إلى مكونات ReactiveSearch التابعة.

سنستخدم هذا في /src/App.js:

import React, { Component } from 'react'; import { ReactiveBase } from '@appbaseio/reactivesearch'; import theme from './theme'; import './App.css'; class App extends Component { render() { return ( GitXplore ); } } export default App;

للحصول على الدعم app، credentialsيمكنك استخدام العناصر التي قدمتها هنا كما هي. إذا قمت باستنساخ مجموعة البيانات في تطبيقك الخاص مسبقًا ، فيمكنك الحصول عليها من صفحة بيانات اعتماد التطبيق. إذا كنت معتادًا على Elasticsearch ، فيمكنك بدلاً من ذلك تمرير urlدعامة تشير إلى عنوان URL الخاص بمجموعة Elasticsearch الخاصة بك.

بدلاً من ذلك ، يمكنك أيضًا نسخ credentialsتطبيقاتك من لوحة معلومات التطبيقات. مرر مؤشر الماوس فوق بطاقة التطبيق الخاص بك وانقر فوق نسخ بيانات الاعتماد المقروءة .

بعد إضافة هذا ، سترى تخطيطًا أساسيًا مثل هذا:

3. إضافة DataSearch

بعد ذلك ، سأضيف مكون DataSearch للبحث في المستودعات. يقوم بإنشاء مكون واجهة مستخدم البحث ويتيح لنا البحث عبر حقل واحد أو أكثر بسهولة. ستبدو renderالوظيفة المحدثة في /src/App.jsالشكل التالي:

// importing DataSearch here import { ReactiveBase, DataSearch } from '@appbaseio/reactivesearch'; ...  // Adding the DataSearch here ...

The DataSearch component goes inside the ReactiveBase component and receives all the necessary data from it so we don’t have to write Elasticsearch queries ourselves. The surrounding divs add some className properties for styling. These just add a layout to the app. You can go through all the styles at /src/App.css which we created earlier. You might have noticed that we have passed some props to the DataSearch component.

Here’s how they work:

  • componentId: a unique string identifier which we’ll use later to connect two different ReactiveSearch components.
  • filterLabel: a string value which will show up in the filters menu later.
  • dataField: an array of strings containing Elasticsearch fields on which search has to performed on. You can check the dataset and see that these fields also matches the column name. All fields specified here matches the structure of data, for example name refers to the name of repo, description refers to its description, but there is a field with a .raw added here, name.raw which is a multi-field of the name field. Elasticsearch can index the same data in different ways for different purposes, which we can use to get better search results.
  • placeholder: sets the placeholder value in the input box.
  • autosuggest: setting a false value for the prop causes the results to update immediately in the results.
  • iconPosition: sets the position of the ? icon.
  • URLParams: is a boolean which tells the component to save the search term in the browser’s URL so we can share a URL to a specific search query. For example, check this link to see all results related to “react”.
  • className: adds a class for styling using CSS.
  • innerClass: adds a class to different sections of a component for styling using CSS. Here, I’ve added a class to the input box for styling. A detailed description can be found in the docs.

With this, our app should get a working search bar:

4. Adding the Results view

Next, we’ll be adding the Results component at /src/components/Results.js and importing it in /src/App.js.

Here’s how you can write the Results component:

import React from 'react'; import { SelectedFilters, ReactiveList } from '@appbaseio/reactivesearch'; const onResultStats = (results, time) => ( {results} results found in {time}ms ); const onData = (data) => ( {data.owner}/{data.name} ); const Results = () => ( ); export default Results;

I’ve imported two new components from ReactiveSearch, SelectedFilters and ReactiveList. SelectedFilters will render the filters for our ReactiveSearch components at one place:

ReactiveList renders the search results. Here’s how its props work:

  • dataField: orders the results using name field here.
  • onData: accepts a function which returns a JSX. The function is passed each result individually. Here we’re generating a basic UI which we’ll modify later.
  • onResultStats: similar to onData but for the result stats. The function is passed the number of results found and time taken.
  • react: the react prop tells the ReactiveList to listen to changes made byCategorySearch component, we’ve provided the componentId of the CategorySearch component here called repo. Later we’ll add more components here.
  • pagination: a boolean which tells the ReactiveList to split the results into pages, each page containing the number of results specified in the size prop.

Now we can import and use the Results component in /src/App.js. Just add it inside the div with results-container class.

... import Results from './components/Results'; ... render() { return( ... ... ) }

With this component, a basic version of our search UI should start coming together:

5. Adding a Header component

Lets create a Header component at /src/components/Header.js which we’ll use to render more search filters.

Here’s how to create a simple Header component:

import React, { Component } from 'react'; import SearchFilters from './SearchFilters'; class Header extends Component { constructor(props) { super(props); this.state = { visible: false, }; } toggleVisibility = () => { const visible = !this.state.visible; this.setState({ visible, }); } render() { return ( GitXplore Toggle Filters ); } } export default Header; 

I’ve moved the navigation code in .. from /src/App.js here. The Header component has a method which toggles visible in the state. We’re using this to add a class which would make it take up the entire screen size on mobile layout. I’ve also added a toggle button which calls the toggleVisibility method.

It also renders another component called SearchFilters and passes all the props from the parent App component. Let’s create this component to see things in action.

Create a new file /src/components/SearchFilters.js:

import React from 'react'; const SearchFilters = () => ( Search filters go here! ); export default SearchFilters;

Next, I’ll update the App component to use the Header component that we created just now.

6. Updating App component and handling topics in state

We’ll add a state variable in App component called currentTopics which would be an array of currently selected topics in the app.

We’ll then use the currentTopics and pass them to the Header and Results components:

import React, { Component } from 'react'; import { ReactiveBase, DataSearch } from '@appbaseio/reactivesearch'; import Header from './components/Header'; import Results from './components/Results'; import theme from './theme'; import './App.css'; class App extends Component { constructor(props) { super(props); this.state = { currentTopics: [], }; } setTopics = (currentTopics) => { this.setState( currentTopics: currentTopics ); } toggleTopic = (topic) => { const { currentTopics } = this.state; const nextState = currentTopics.includes(topic) ? currentTopics.filter(item => item !== topic) : currentTopics.concat(topic); this.setState({ currentTopics: nextState, }); } render() { return ( ); } } export default App;

The setTopics method will set whichever topics are passed to it, which we’ll pass to the Header component. The toggleTopic method will remove a topic from the state in currentTopics if it’s already present and add the topic if it is not present.

We’ll pass the toggleTopic method to the Results component:

7. Adding more filters

Lets add more filters to the UI in /src/components/SearchFilters.js. I’ll be using three new components from ReactiveSearch here, MultiDropdownList, SingleDropdownRange and RangeSlider. The components are used in a similar fashion as we used the DataSearch component earlier.

Here’s the code:

import React from 'react'; import PropTypes from 'prop-types'; import { MultiDropdownList, SingleDropdownRange, RangeSlider, } from '@appbaseio/reactivesearch'; const SearchFilters = ({ currentTopics, setTopics, visible }) => ( ); SearchFilters.propTypes = { currentTopics: PropTypes.arrayOf(PropTypes.string), setTopics: PropTypes.func, visible: PropTypes.bool, }; export default SearchFilters; 

The SearchFilters component we’ve created above takes in three props from the Header component, currentTopics, setTopics and visible. The visible prop is just used to add a className for styling.

The first component we’ve used here is a MultiDropdownList which renders a dropdown component to select multiple options. The first MultiDropdownList has a dataField of language.raw. It’ll populate itself with all the languages available in the repositories dataset.

We’ve used another MultiDropdownList to render a list of topics:

Here’s how the props work here:

  • componentId: similar to the previous ReactiveSearch components, this is a unique identifier which we’ll later associate in the Results component that we created to get search results.
  • dataField: maps the component to the topics.raw field in Elasticsearch.
  • placeholder: sets the placeholder value when nothing is selected.
  • title: adds a title for the component in the UI.
  • filterLabel: sets the label of the components in the removable filters (the SelectedFilters which we used in the Results component).
  • size: tells the component to render a maximum of 1000 items in the list.
  • queryFormat: when set to 'and' as we’ve used here, it gives results which matches all the selected tags (exactly like intersection).
  • defaultSelected: sets the selected items in the component. Here we’re passing currentTopics which we’ve stored in the state at /src/App.js.
  • onValueChange: is a function that will be called by the component when we make a change in its value. Here we call the setTopics function which we received in the props. Therefore, whenever we select or deselect a value in the component it would update the currentTopics in the state of main App component.

The next ReactiveSearch component we’ve used here is a SingleDropdownRange. It uses a new prop called data.

Here’s how it works:

The data prop accepts an array of objects with start and end values and shows the specified label in the dropdown. It’s mapped to the pushed field in the dataset which is a date type in Elasticsearch. One cool way to specify date range in Elasticsearch is using the now keyword. now refers to the current time, now-1M refers to one month before, now-6M to six month before and now-1y to a year before now.

I’ve used another SingleDropdownRange component for the created field in the dataset.

Here I’ve specified year ranges in datetime for different years:

The third component I’ve used is a RangeSlider which renders a slider UI. I’ve used to RangeSlider components, one for the stars field and the other for forks.

Two main props that this component introduces are range and rangeLabels:

  • range: prop specifies a range for the data with a start and end value.
  • rangeLabels: prop takes the labels to show below the slider.
  • showHistogram: is a boolean prop which shows a histogram with the distribution of data. Here I’ve set it to false since it’s not needed.

Now we just need to connect these filters to the Results component. We just have to update one line in the ReactiveList rendered by the Results component to include the componentIds of these components.

Update the react prop in the ReactiveList that we rendered in the Results component:

const Results = () => ( );

That should make your results update for all the filters ?

8. Updating the results view

Up until now, we’ve been seeing only a basic version of the results. As the final piece of this app, lets add some flair to the results ✌️

We’ll be using another component inside our Results components to render different topics.

Here’s how you can create your own at /src/components/Topic. Feel free to add your own taste ?

 import React, { Component } from 'react'; import PropTypes from 'prop-types'; class Topic extends Component { handleClick = () => { this.props.toggleTopic(this.props.children); } render() { return ( #{this.props.children} ); } } Topic.propTypes = { children: PropTypes.string, active: PropTypes.bool, toggleTopic: PropTypes.func, }; export default Topic;

This component renders its children and adds a click handler to toggle the topics which updates the currentTopics inside the main App component’s state.

Next, we just need to update our Results component at /src/components/Results.js:

import React from 'react'; import { SelectedFilters, ReactiveList } from '@appbaseio/reactivesearch'; import PropTypes from 'prop-types'; import Topic from './Topic'; const onResultStats = (results, time) => ( {results} results found in {time}ms ); const onData = (data, currentTopics, toggleTopic) => (  {data.owner}/ {data.name} {data.description} { data.topics.slice(0, 7) .map(item => (  {item}  )) } {data.stars} {data.forks} {data.watchers} ); const Results = ({ toggleTopic, currentTopics }) => ( onData(data, currentTopics, toggleTopic)} onResultStats={onResultStats} react={{ and: ['language', 'topics', 'pushed', 'created', 'stars', 'forks', 'repo'], }} pagination innerClass={{ list: 'result-list-container', pagination: 'result-list-pagination', resultsInfo: 'result-list-info', poweredBy: 'powered-by', }} size={6} sortOptions={[ { label: 'Best Match', dataField: '_score', sortBy: 'desc', }, { label: 'Most Stars', dataField: 'stars', sortBy: 'desc', }, { label: 'Fewest Stars', dataField: 'stars', sortBy: 'asc', }, { label: 'Most Forks', dataField: 'forks', sortBy: 'desc', }, { label: 'Fewest Forks', dataField: 'forks', sortBy: 'asc', }, { label: 'A to Z', dataField: 'owner.raw', sortBy: 'asc', }, { label: 'Z to A', dataField: 'owner.raw', sortBy: 'desc', }, { label: 'Recently Updated', dataField: 'pushed', sortBy: 'desc', }, { label: 'Least Recently Updated', dataField: 'pushed', sortBy: 'asc', }, ]} /> ); Results.propTypes = { toggleTopic: PropTypes.func, currentTopics: PropTypes.arrayOf(PropTypes.string), }; export default Results;

I’ve updated the onData function to render more detailed results. You’ll also notice a new sortOptions prop in the ReactiveList. This prop accepts an array of objects which renders a dropdown menu to select how you wish to sort the results. Each object contains a label to display as the list item, a dataField to sort the results on and a sortBy key which can either be asc (ascending) or desc (descending).

That’s it, your very own GitHub repository explorer should be live!

Useful links

  1. GitXplore app demo, CodeSandbox and source code
  2. ReactiveSearch GitHub repo
  3. ReactiveSearch docs

Hope you enjoyed this story. If you have any thoughts or suggestions, please let me know and do share your version of the app in comments!

يمكنك متابعتي على تويتر للحصول على آخر التحديثات. لقد بدأت أيضًا في نشر المزيد من المشاركات الحديثة على مدونتي الشخصية.