بحث نصي بسيط وخالي من الفهرس لـ JavaScript، يُستخدم عبر مشاريعي الشخصية مثل YC Vibe Check وlinus.zone/entr وبرنامج الإنتاجية الشخصي الخاص بي. اقرأ المصدر المشروح لفهم كيفية عمله تحت الغطاء.
لنبدأ ببعض الأمثلة السريعة:
import { search } from 'libsearch' ; // on Node.js
const { search } = window . libsearch ; // in the browser
const articles = [
{ title : 'Weather in Berkeley, California' } ,
{ title : 'University report: UC Berkeley' } ,
{ title : 'Berkeley students rise in solidarity...' } ,
{ title : 'Californian wildlife returning home' } ,
] ;
// basic usage
search ( articles , 'berkeley cali' , a => a . title ) ;
// => [{ title: 'Weather in Berkeley, California' }]
search ( articles , 'california' , a => a . title ) ;
// => [
// { title: 'Weather in Berkeley, California' },
// { title: 'Californian wildlife returning home' },
// ]
// mode: 'word' only returns whole-word matches
search ( articles , 'california' , a => a . title , { mode : 'word' } ) ;
// => [{ title: 'Weather in Berkeley, California' }]
// case sensitivity
search ( articles , 'W' , a => a . title , { caseSensitive : true } ) ;
// => [{ title: 'Weather in Berkeley, California' }]
// empty query returns the full list, unmodified
search ( articles , '' , a => a . title ) ;
// => [{...}, {...}, {...}, {...}]
وبشكل أكثر رسمية، يكشف libsearch عن واجهة برمجة تطبيقات واحدة، وهي وظيفة search
. تأخذ هذه الدالة وسيطتين مطلوبتين ووسيطين اختياريين:
function search < T > (
items : T [ ] ,
query : string ,
by ?: ( it : T ) => string ,
options ?: {
caseSensitive : boolean ,
mode : 'word' | 'prefix' | 'autocomplete' ,
} ,
) : T [ ]
items
هي قائمة العناصر للبحث. عادةً ما تكون items
عبارة عن مصفوفة من السلاسل أو مصفوفة من الكائنات التي تحتوي على بعض خصائص السلسلة.query
هو استعلام سلسلة يمكن من خلاله البحث في قائمة العناصر.by
( اختياري ) هي دالة أصلية تأخذ عنصرًا من items
وترجع قيمة سلسلة يمكن من خلالها البحث عن هذا العنصر. على سبيل المثال، إذا كانت items
عبارة عن قائمة من الكائنات مثل { name: 'Linus' }
، فيجب أن تكون by
دالة x => x.name
. يحتوي هذا على القيمة x => String(x)
افتراضيًا، والتي تعمل مع items
من النوع string[]
.options
( اختياري ) عبارة عن قاموس للخيارات:caseSensitive
يجعل البحث حساسًا لحالة الأحرف. انها false
افتراضيا.mode
في الطريقة التي تتم بها مطابقة كلمات الاستعلام غير المكتملة:mode: 'word'
أن تتطابق كل كلمة استعلام مع الكلمات الكاملة والدقيقة فقط وليس أجزاء من الكلمات. على سبيل المثال، سيتطابق الاستعلام "California" مع "University of California " ولكن لن يطابق " California n University".mode: 'prefix'
أن كل كلمة استعلام قد تكون "بادئة" غير كاملة للكلمة المطابقة. ستتطابق "Uni Cali" مع كل من " Uni versity of Cali fornia" و" Cali fornian Uni versity" حتى في هذا الوضع، يجب أن تتطابق كل كلمة استعلام في مكان ما - " Cali fornia" ليست متطابقة، لأنها لا تتطابق مع الاستعلام كلمة "يوني".mode: 'autocomplete'
عبارة عن مزيج من الوضعين الآخرين وهو مفيد عند استخدامه في عمليات البحث بنمط الإكمال التلقائي، حيث يقوم المستخدم بكتابة استعلام بشكل مستمر أثناء عرض نتائج البحث. هذا الوضع مطابق mode: 'word'
، باستثناء أن كلمة الاستعلام الأخيرة قد تكون غير مكتملة كما هو الحال في mode: 'prefix'
. وهذا يعني أن "University of Cali" سوف يتطابق مع " University of Cali fornia"، وهو أمر مفيد لأن المستخدم قد يجد التطابق الخاص به قبل كتابة الاستعلام الكامل الخاص به.يمكنك العثور على المزيد من الأمثلة حول كيفية دمج هذه الخيارات معًا في اختبارات الوحدة.
<script>
قم بإسقاط هذا في HTML الخاص بك:
< script src =" https://unpkg.com/libsearch/dist/browser.js " > </ script >
سيؤدي هذا إلى كشف وظيفة search
على أنها window.libsearch.search
.
npm install libsearch
# or
yarn add libsearch
واستخدامها في التعليمات البرمجية الخاصة بك:
import { search } from 'libsearch' ;
// search(...);
يأتي libsearch مع تعريفات نوع TypeScript التي تم إنشاؤها من الملف المصدر. إن استخدام libsearch من NPM يجب أن يلتقطها مترجم TypeScript.
يتيح لك libsearch إجراء بحث أساسي عن النص الكامل عبر قائمة كائنات JavaScript بسرعة، دون الحاجة إلى فهرس بحث مُعد مسبقًا، مع تقديم تصنيف جيد إلى حد معقول لنتائج TF-IDF. لا يقدم مجموعة واسعة من الميزات التي تأتي مع مكتبات مثل FlexSearch وlunr.js، ولكنه يمثل خطوة كبيرة فوق text.indexOf(query) > -1
، كما أنه سريع بما يكفي ليكون قابلاً للاستخدام للبحث في آلاف المستندات على كل ضغطة مفتاح في تجربتي.
هناك فكرتان رئيسيتان حول كيفية تقديم libsearch لهذا:
تأتي محركات JavaScript الحديثة مزودة بمحركات تعبير عادية محسنة للغاية، ويستفيد libsearch من ذلك لإجراء بحث نصي سريع وخالي من الفهرس عن طريق تحويل سلاسل الاستعلام إلى مرشحات تعبير عادي في وقت البحث.
تعمل معظم مكتبات البحث عن النص الكامل من خلال مطالبة المطور أولاً بإنشاء بنية بيانات "فهرس" لتعيين مصطلحات البحث للمستندات التي تظهر فيها. عادةً ما تكون هذه مقايضة جيدة، لأنها تحرك بعض الأعمال الحسابية "للبحث" ليتم إنجازها مسبقًا، لذلك يمكن أن يظل البحث نفسه سريعًا ودقيقًا. كما أنه يسمح بإجراء تحويلات رائعة وتنظيف البيانات مثل إضفاء الطابع الشخصي على البيانات المفهرسة دون تدمير سرعة البحث. ولكن عند إنشاء نماذج أولية وتطبيقات ويب بسيطة، لم أرغب غالبًا في تحمل التعقيد المتمثل في وجود خطوة "فهرسة" منفصلة للحصول على حل بحث "جيد بما فيه الكفاية". يجب تخزين الفهرس في مكان ما وصيانته باستمرار مع تغير مجموعة البيانات الأساسية ونموها.
تتمثل المهمة الرئيسية لفهرس البحث في تعيين "الرموز المميزة" أو الكلمات الرئيسية التي تظهر في مجموعة البيانات إلى المستندات التي تظهر فيها، بحيث يتم طرح السؤال "ما هي المستندات التي تحتوي على الكلمة X؟" سريع ( O(1)
) للإجابة في وقت البحث. بدون فهرس، يتحول هذا إلى سؤال O(n)
، حيث يجب فحص كل مستند بحثًا عن الكلمة الأساسية. ولكن في كثير من الأحيان، على الأجهزة الحديثة، بالنسبة لمجموعات البيانات الصغيرة بما يكفي (بضعة ميغابايت) النموذجية في تطبيق الويب من جانب العميل، يكون n
صغيرًا جدًا، وصغيرًا بدرجة كافية بحيث لا يمكن ملاحظة O(n)
في كل ضغطة مفتاح.
يقوم libsearch بتحويل استعلام مثل "Uni of California" إلى قائمة من مرشحات التعبير العادي، (^|W)Uni($|W)
, (^|W)of($|W)
, (^|W)California
. ثم يقوم بعد ذلك "بالبحث" دون الحاجة إلى فهرس عن طريق تصفية المجموعة عبر كل من تلك التعبيرات العادية.
يتم حساب مقياس TF-IDF التقليدي لكل كلمة على النحو التالي:
( # matches ) / ( # words in the doc ) * log ( # total docs / # docs that matched )
يتطلب الحصول على عدد الكلمات في مستند ترميز المستند، أو على الأقل تقسيم المستند بمسافات بيضاء، وهو أمر مكلف من الناحية الحسابية. لذلك يقوم libsearch بتقريب ذلك باستخدام طول المستند (عدد الأحرف) بدلاً من ذلك.
باستخدام استعلامات التعبير العادي الموضحة أعلاه، فإن صيغة TF-IDF الخاصة بـ libsearch هي:
( # RegExp matches ) / ( doc . length ) * log ( # docs / # docs that matched RegExp )
والتي يتم حسابها لكل كلمة أثناء إجراء البحث، ثم يتم تجميعها في النهاية للفرز.
الكود المصدري لـ libsearch مكتوب بلغة TypeScript. للسماح باستخدام المكتبة عبر TypeScript وVilla Node.js والويب، قمنا بتجميع نسختين:
search.ts
فيها فقط وإزالة الأنواع. هذا هو الكود الذي تم استيراده عند استيراد libsearch
في Node.jssearch
الرئيسية إلى window.libsearch
العالمية يتم إنتاج بنية الوحدة ES باستخدام tsc
، ومترجم TypeScript، ويتم إنتاج بنية المتصفح المصغرة أيضًا باستخدام Webpack.
أوامر NPM/الغزل:
lint
و fmt
، اللذان يقومان بتنسيق التعليمات البرمجية المصدرية تلقائيًا في المستودعtest
تشغيل اختبارات الوحدة على أحدث إصدار للمكتبة؛ يجب عليك تشغيل build:tsc
قبل تشغيل test
build:*
تقوم الأوامر بتنسيق إنتاج الأنواع المختلفة من بنيات المكتبة:build:tsc
يبني بناء الوحدة النمطية ESbuild:w
بتشغيل build:tsc
في كل ملف يتم كتابتهbuild:cjs
يبني المتصفح من بناء الوحدة النمطية ESbuild:all
يبني كلا البناءين بالترتيبclean
جميع الملفات التي تم إنشاؤها/إنشاءها في dist/
docs
بإنشاء الوثائق المستندة إلى Litterate، والتي توجد على موقع thisphist.github.io/libsearch.قبل الانتقال إلى الصفحة الرئيسية أو النشر، عادةً ما أقوم بالتشغيل
yarn fmt && yarn build:all && yarn test && yarn docs
للتأكد من أنني لم أنس أي شيء.