Pencarian Teks Lengkap di Android

Dalam aplikasi seluler, fungsi pencarian sangat populer. Dan jika dapat diabaikan dalam produk kecil, maka dalam aplikasi yang menyediakan akses ke sejumlah besar informasi, Anda tidak dapat melakukannya tanpa pencarian. Hari ini saya akan memberi tahu Anda cara menerapkan fungsi ini dengan benar dalam program untuk Android.



Pendekatan pelaksanaan pencarian dalam aplikasi seluler


  1. Cari sebagai filter data

    Biasanya terlihat seperti bilah pencarian di atas beberapa daftar. Yaitu, kami hanya memfilter data yang sudah jadi.
  2. Pencarian server

    Dalam hal ini, kami memberikan seluruh implementasi ke server, dan aplikasi bertindak sebagai klien tipis, yang hanya diperlukan untuk menampilkan data dalam bentuk yang benar.
  3. Pencarian Terintegrasi

    • aplikasi berisi sejumlah besar data dari berbagai jenis;
    • aplikasi ini bekerja secara offline;
    • Pencarian diperlukan sebagai titik akses tunggal ke bagian / konten aplikasi.

Dalam kasus terakhir, pencarian teks lengkap dibangun ke dalam SQLite datang untuk menyelamatkan. Dengannya, Anda dapat dengan cepat menemukan kecocokan dalam sejumlah besar informasi, yang memungkinkan kami membuat beberapa kueri ke berbagai tabel tanpa mengorbankan kinerja.

Pertimbangkan implementasi pencarian seperti itu menggunakan contoh spesifik.

Persiapan data


Katakanlah kita perlu mengimplementasikan aplikasi yang menampilkan daftar film dari themoviedb.org . Untuk menyederhanakan (agar tidak online), ambil daftar film dan bentuk file JSON darinya, masukkan ke dalam aset dan isi basis data kami secara lokal.

Contoh struktur file JSON:

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

Pengisian Basis Data


SQLite menggunakan tabel virtual untuk mengimplementasikan pencarian teks lengkap. Di luar, mereka terlihat seperti tabel SQLite biasa, tetapi akses apa pun ke mereka melakukan beberapa pekerjaan di belakang panggung.

Tabel virtual memungkinkan kita untuk mempercepat pencarian. Namun, selain kelebihannya, mereka juga memiliki kelemahan:

  • Anda tidak dapat membuat pemicu di tabel virtual;
  • Anda tidak dapat menjalankan perintah ALTER TABLE dan ADD COLUMN untuk tabel virtual;
  • setiap kolom dalam tabel virtual diindeks, yang berarti bahwa sumber daya dapat terbuang sia-sia pada kolom pengindeksan yang tidak boleh terlibat dalam pencarian.

Untuk mengatasi masalah yang terakhir, Anda bisa menggunakan tabel tambahan yang akan berisi bagian dari informasi, dan menyimpan tautan ke elemen tabel biasa di tabel virtual.

Membuat tabel sedikit berbeda dari standar, kami memiliki kata kunci VIRTUAL dan fts4 :

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

Mengomentari versi fts5
Sudah ditambahkan ke SQLite. Versi ini lebih produktif, lebih akurat dan mengandung banyak fitur baru. Tetapi karena fragmentasi Android yang besar, kami tidak dapat menggunakan fts5 (tersedia dengan API24) di semua perangkat. Anda dapat menulis logika berbeda untuk versi berbeda dari sistem operasi, tetapi ini akan secara serius mempersulit pengembangan dan dukungan lebih lanjut. Kami memutuskan untuk menggunakan cara yang lebih mudah dan menggunakan fts4, yang didukung pada sebagian besar perangkat.

Mengisi tidak berbeda dari biasanya:

 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() } } 

Versi dasar


Saat menjalankan kueri, kata kunci MATCH digunakan alih-alih 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 } 

Untuk mengimplementasikan pemrosesan input teks di antarmuka, kita akan menggunakan 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) 

Hasilnya adalah opsi pencarian dasar. Pada elemen pertama, kata yang diinginkan ditemukan dalam deskripsi, dan pada elemen kedua baik dalam judul maupun dalam deskripsi. Jelas, dalam formulir ini tidak sepenuhnya jelas apa yang kami temukan. Mari kita perbaiki.


Tambahkan aksen


Untuk meningkatkan kejelasan pencarian, kami akan menggunakan fungsi bantu SNIPPET . Ini digunakan untuk menampilkan fragmen teks yang diformat tempat kecocokan ditemukan.

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

  • film - nama tabel;
  • <b & gt dan </b> - argumen ini digunakan untuk menyorot bagian teks yang dicari;
  • ... - untuk desain teks, jika hasilnya adalah nilai yang tidak lengkap;
  • 1 - nomor kolom tabel dari mana teks akan dialokasikan;
  • 15 adalah perkiraan jumlah kata yang termasuk dalam nilai teks yang dikembalikan.

Kode identik dengan yang pertama, tidak termasuk permintaan:

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

Kami coba lagi:


Ternyata lebih jelas daripada di versi sebelumnya. Tapi ini bukan akhirnya. Mari kita membuat pencarian kita lebih "lengkap." Kami akan menggunakan analisis leksikal dan menyoroti bagian penting dari permintaan pencarian kami.

Selesaikan perbaikan


SQLite memiliki token bawaan yang memungkinkan Anda untuk melakukan analisis leksikal dan mengubah permintaan pencarian asli. Jika saat membuat tabel kami tidak menentukan tokenizer tertentu, maka "sederhana" akan dipilih. Bahkan, itu hanya mengubah data kami menjadi huruf kecil dan membuang karakter yang tidak dapat dibaca. Itu tidak cocok untuk kita.

Untuk peningkatan kualitatif dalam pencarian, kita perlu menggunakan stemming - proses menemukan basis kata untuk kata sumber yang diberikan.

SQLite memiliki tokenizer built-in tambahan yang menggunakan algoritma Porter Stemmer. Algoritma ini secara berurutan menerapkan sejumlah aturan tertentu, menyoroti bagian-bagian penting dari sebuah kata dengan memotong akhiran dan sufiks. Misalnya, saat mencari "kunci", kita bisa mencari di mana kata "kunci", "kunci" dan "kunci" ada. Saya akan meninggalkan tautan ke deskripsi terperinci tentang algoritma di bagian akhir.

Sayangnya, tokenizer yang ada di dalam SQLite hanya berfungsi dengan bahasa Inggris, jadi untuk bahasa Rusia Anda perlu menulis implementasi Anda sendiri atau menggunakan pengembangan yang sudah jadi. Kami akan mengambil implementasi yang sudah selesai dari situs algorithmist.ru .

Kami mengubah permintaan pencarian kami ke dalam formulir yang diperlukan:

  1. Hapus karakter tambahan.
  2. Pecahkan frasa menjadi kata-kata.
  3. Lewati stemmer.
  4. Kumpulkan dalam permintaan pencarian.

Algoritma Porter

  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 } } 

Algoritma tempat kita memecah frasa menjadi kata-kata

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

Setelah konversi ini, frasa "halaman dan hantu" terlihat seperti "halaman * ATAU hantu * ".

Simbol " * " berarti bahwa pencarian akan dilakukan oleh kemunculan kata yang diberikan dengan kata lain. Operator " ATAU " berarti bahwa hasil akan ditampilkan yang mengandung setidaknya satu kata dari frasa pencarian. Kami melihat:



Ringkasan


Pencarian teks lengkap tidak serumit yang tampak pada pandangan pertama. Kami telah menganalisis contoh spesifik yang dapat Anda implementasikan dengan cepat dan mudah dalam proyek Anda. Jika Anda memerlukan sesuatu yang lebih rumit, maka Anda harus membuka dokumentasi, karena ada satu dan itu ditulis dengan cukup baik.

Referensi:


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


All Articles