بحث النص الكامل في الروبوت

في تطبيقات الهاتف المحمول ، تحظى وظيفة البحث بشعبية كبيرة. وإذا كان بالإمكان إهمالها في المنتجات الصغيرة ، ففي التطبيقات التي توفر الوصول إلى كمية كبيرة من المعلومات ، لا يمكنك الاستغناء عن البحث. سأخبرك اليوم عن كيفية تنفيذ هذه الوظيفة بشكل صحيح في برامج Android.



نهج لتنفيذ البحث في تطبيق المحمول


  1. البحث كعامل تصفية بيانات

    عادة ما يشبه شريط البحث أعلى قائمة. وهذا هو ، نحن فقط تصفية البيانات النهائية.
  2. خادم البحث

    في هذه الحالة ، نعطي التطبيق بالكامل للخادم ، ويعمل التطبيق كعميل رفيع ، والذي من الضروري فقط عرض البيانات في النموذج الصحيح.
  3. بحث متكامل

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

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

النظر في تنفيذ مثل هذا البحث باستخدام مثال محدد.

إعداد البيانات


لنفترض أننا بحاجة إلى تنفيذ تطبيق يعرض قائمة الأفلام من themoviedb.org . لتبسيط (حتى لا تكون متصلاً بالإنترنت) ، خذ قائمة بالأفلام وشكل ملف JSON منه ، ضعه في أصول ، وقم بتعبئة قاعدة بياناتنا محليًا.

مثال بنية ملف JSON:

[ { "id": 278, "title": "  ", "overview": "  ..." }, { "id": 238, "title": " ", "overview": " , ..." }, { "id": 424, "title": " ", "overview": "   ..." } ] 

ملء قاعدة البيانات


يستخدم SQLite الجداول الافتراضية لتنفيذ البحث عن النص الكامل. ظاهريا ، تبدو مثل جداول SQLite العادية ، ولكن أي وصول إليها يؤدي بعض الأعمال وراء الكواليس.

تسمح الجداول الافتراضية لنا بتسريع عملية البحث. ولكن ، بالإضافة إلى المزايا ، لديهم أيضًا عيوب:

  • لا يمكنك إنشاء مشغل على جدول افتراضي ؛
  • لا يمكنك تنفيذ أوامر ALTER TABLE و ADD COLUMN لجدول افتراضي ؛
  • تتم فهرسة كل عمود في الجدول الافتراضي ، مما يعني أنه يمكن إهدار الموارد على أعمدة الفهرسة التي يجب ألا تشارك في البحث.

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

إنشاء جدول مختلف قليلاً عن المعيار ، لدينا الكلمات الأساسية VIRTUAL و fts4 :

  CREATE VIRTUAL TABLE movies USING fts4(id, title, overview); 

التعليق على إصدار fts5
لقد تم بالفعل إضافتها إلى SQLite. هذا الإصدار أكثر إنتاجية وأكثر دقة ويحتوي على الكثير من الميزات الجديدة. ولكن نظرًا للتجزؤ الكبير لنظام Android ، لا يمكننا استخدام fts5 (المتوفر مع API24) على جميع الأجهزة. يمكنك كتابة منطق مختلف للإصدارات المختلفة من نظام التشغيل ، ولكن هذا سيعقد بشكل خطير المزيد من التطوير والدعم. قررنا أن نذهب أسهل الطرق واستخدام fts4 ، وهو معتمد على معظم الأجهزة.

التعبئة لا تختلف عن المعتاد:

 fun populate(context: Context) { val movies: MutableList<Movie> = mutableListOf() context.assets.open("movies.json").use { val typeToken = object : TypeToken<List<Movie>>() {}.type movies.addAll(Gson().fromJson(InputStreamReader(it), typeToken)) } try { writableDatabase.beginTransaction() movies.forEach { movie -> val values = ContentValues().apply { put("id", movie.id) put("title", movie.title) put("overview", movie.overview) } writableDatabase.insert("movies", null, values) } writableDatabase.setTransactionSuccessful() } finally { writableDatabase.endTransaction() } } 

الإصدار الأساسي


عند تنفيذ الاستعلام ، يتم استخدام الكلمة الأساسية MATCH بدلاً من LIKE :

 fun firstSearch(searchString: String): List<Movie> { val query = "SELECT * FROM movies WHERE movies MATCH '$searchString'" val cursor = readableDatabase.rawQuery(query, null) val result = mutableListOf<Movie>() cursor?.use { if (!cursor.moveToFirst()) return result while (!cursor.isAfterLast) { val id = cursor.getInt("id") val title = cursor.getString("title") val overview = cursor.getString("overview") result.add(Movie(id, title, overview)) cursor.moveToNext() } } return result } 

لتنفيذ معالجة إدخال النص في الواجهة ، سنستخدم RxJava :

 RxTextView.afterTextChangeEvents(findViewById(R.id.editText)) .debounce(500, TimeUnit.MILLISECONDS) .map { it.editable().toString() } .filter { it.isNotEmpty() && it.length > 2 } .map(dbHelper::firstSearch) .subscribeOn(Schedulers.computation()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(movieAdapter::updateMovies) 

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


إضافة لهجات


لتحسين وضوح البحث ، سنستخدم الوظيفة الإضافية SNIPPET . يتم استخدامه لعرض جزء النص المنسق الذي يتم العثور على تطابق.

 snippet(movies, '<b>', '</b>', '...', 1, 15) 

  • الأفلام - اسم الجدول ؛
  • <b & gt و </b> - تُستخدم هذه الوسيطات لتمييز قسم من النص تم البحث فيه ؛
  • ... - للنص ، إذا كانت النتيجة قيمة غير مكتملة ؛
  • 1 - رقم عمود الجدول الذي سيتم تخصيص أجزاء النص منه ؛
  • 15 هو عدد تقريبي للكلمات المضمنة في قيمة النص الذي تم إرجاعه.

الكود مطابق للرمز الأول ، دون حساب الطلب:

 SELECT id, snippet(movies, '<b>', '</b>', '...', 1, 15) title, snippet(movies, '<b>', '</b>', '...', 2, 15) overview FROM movies WHERE movies MATCH '' 

نحاول مرة أخرى:


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

الانتهاء من التحسن


يحتوي SQLite على رموز مضمنة تتيح لك إجراء تحليل معجمي وتحويل استعلام البحث الأصلي. إذا لم نحدد رمز مميز عند إنشاء الجدول ، فسيتم اختيار "بسيط". في الحقيقة ، إنه يحول بياناتنا إلى أحرف صغيرة ويتجاهل الأحرف غير القابلة للقراءة. هذا لا يناسبنا تمامًا.

للحصول على تحسين نوعي في البحث ، نحتاج إلى استخدام stemming - وهي عملية العثور على قاعدة الكلمة لكلمة مصدر معينة.

يحتوي SQLite على رمز مميز إضافي يستخدم خوارزمية Porter Stemmer. تطبق هذه الخوارزمية بالتتابع عددًا من القواعد المعينة ، مع إبراز أجزاء كبيرة من الكلمة عن طريق قطع النهايات واللواحق. على سبيل المثال ، عند البحث عن "مفاتيح" ، يمكننا الحصول على بحث يحتوي على الكلمات "مفتاح" و "مفاتيح" و "مفتاح". سأترك رابطًا لوصف تفصيلي للخوارزمية في النهاية.

لسوء الحظ ، فإن الرمز المميز المضمّن في SQLite لا يعمل إلا مع اللغة الإنجليزية ، لذلك تحتاج إلى كتابة التطبيق الخاص بك أو استخدام التطورات الجاهزة للغة الروسية. سنتخذ التنفيذ النهائي من موقع algorithmist.ru .

نقوم بتحويل استعلام البحث الخاص بنا إلى النموذج الضروري:

  1. إزالة أحرف إضافية.
  2. قسّم العبارة إلى كلمات.
  3. القفز من خلال stemmer.
  4. جمع في استعلام البحث.

خوارزمية بورتر

  object Porter { private val PERFECTIVEGROUND = Pattern.compile("((|||||)|((<=[])(||)))$") private val REFLEXIVE = Pattern.compile("([])$") private val ADJECTIVE = Pattern.compile("(|||||||||||||||||||||||||)$") private val PARTICIPLE = Pattern.compile("((||)|((?<=[])(||||)))$") private val VERB = Pattern.compile("((||||||||||||||||||||||||||||)|((?<=[])(||||||||||||||||)))$") private val NOUN = Pattern.compile("(|||||||||||||||||||||||||||||||||||)$") private val RVRE = Pattern.compile("^(.*?[])(.*)$") private val DERIVATIONAL = Pattern.compile(".*[^]+[].*?$") private val DER = Pattern.compile("?$") private val SUPERLATIVE = Pattern.compile("(|)$") private val I = Pattern.compile("$") private val P = Pattern.compile("$") private val NN = Pattern.compile("$") fun stem(words: String): String { var word = words word = word.toLowerCase() word = word.replace('', '') val m = RVRE.matcher(word) if (m.matches()) { val pre = m.group(1) var rv = m.group(2) var temp = PERFECTIVEGROUND.matcher(rv).replaceFirst("") if (temp == rv) { rv = REFLEXIVE.matcher(rv).replaceFirst("") temp = ADJECTIVE.matcher(rv).replaceFirst("") if (temp != rv) { rv = temp rv = PARTICIPLE.matcher(rv).replaceFirst("") } else { temp = VERB.matcher(rv).replaceFirst("") if (temp == rv) { rv = NOUN.matcher(rv).replaceFirst("") } else { rv = temp } } } else { rv = temp } rv = I.matcher(rv).replaceFirst("") if (DERIVATIONAL.matcher(rv).matches()) { rv = DER.matcher(rv).replaceFirst("") } temp = P.matcher(rv).replaceFirst("") if (temp == rv) { rv = SUPERLATIVE.matcher(rv).replaceFirst("") rv = NN.matcher(rv).replaceFirst("") } else { rv = temp } word = pre + rv } return word } } 

الخوارزمية حيث نقسم العبارة إلى كلمات

 val words = searchString .replace("\"(\\[\"]|.*)?\"".toRegex(), " ") .split("[^\\p{Alpha}]+".toRegex()) .filter { it.isNotBlank() } .map(Porter::stem) .filter { it.length > 2 } .joinToString(separator = " OR ", transform = { "$it*" }) 

بعد هذا التحويل ، تبدو عبارة "الأفنية والأشباح" مثل "الفناء * أو الأشباح * ".

الرمز " * " يعني أن البحث سيتم من خلال حدوث كلمة معينة بمعنى آخر. يعني عامل التشغيل " OR " أنه سيتم عرض النتائج التي تحتوي على كلمة واحدة على الأقل من عبارة البحث. نحن ننظر:



ملخص


البحث عن النص الكامل ليس معقدًا كما قد يبدو للوهلة الأولى. لقد قمنا بتحليل مثال محدد يمكنك تنفيذه بسرعة وسهولة في مشروعك. إذا كنت بحاجة إلى شيء أكثر تعقيدًا ، فعليك اللجوء إلى الوثائق ، لأن هناك واحدًا وهو مكتوب جيدًا.

المراجع:


Source: https://habr.com/ru/post/ar466787/


All Articles