core: гибридный поиск (BM25 + вектор + RRF)
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
Проект: /root/WORK/memo (Kotlin/JVM). Заказ по поиску.
|
||||
|
||||
Создать РОВНО ДВА новых файла:
|
||||
memo-core/src/main/kotlin/memo/core/Searcher.kt
|
||||
memo-core/src/test/kotlin/memo/core/SearcherTest.kt
|
||||
|
||||
## Searcher.kt
|
||||
|
||||
package memo.core
|
||||
|
||||
enum class SearchMode { HYBRID, LEX, VEC }
|
||||
|
||||
data class Hit(
|
||||
val path: String,
|
||||
val line: Int,
|
||||
val heading: String,
|
||||
val score: Double,
|
||||
val text: String, // текст чанка, обрезанный до 1200 символов
|
||||
)
|
||||
|
||||
fun interface RefreshHook { fun refresh(root: java.io.File) }
|
||||
|
||||
class Searcher(
|
||||
private val db: Db,
|
||||
private val embedder: Embedder,
|
||||
private val refresh: RefreshHook? = null,
|
||||
) {
|
||||
/** Ищет по индексу. Если задан [refresh] — сначала вызывает его для root
|
||||
* (догон свежих правок файлов до поиска). */
|
||||
fun search(root: java.io.File, query: String, k: Int = 8, mode: SearchMode = SearchMode.HYBRID): List<Hit>
|
||||
}
|
||||
|
||||
Правила реализации:
|
||||
1. Если refresh != null — вызвать refresh.refresh(root) ПЕРЕД любым запросом к базе.
|
||||
2. Лексический список (режимы LEX и HYBRID): разбить query на токены по всем символам, кроме букв
|
||||
и цифр (unicode: Character.isLetterOrDigit); оставить токены длиной >= 2; взять первые 8;
|
||||
собрать строку вида "ток1" OR "ток2" (каждый токен в двойных кавычках);
|
||||
если токенов нет — лексический список пуст, без ошибки.
|
||||
Запрос: SELECT rowid, bm25(chunks_fts) AS s FROM chunks_fts WHERE chunks_fts MATCH ? ORDER BY s LIMIT 32.
|
||||
Ошибку SQLite на MATCH не ронять наружу, а считать списком пустым.
|
||||
3. Векторный список (режимы VEC и HYBRID): embedder.embed(query) → FloatArray[768];
|
||||
запрос: SELECT rowid, distance FROM chunks_vec WHERE embedding MATCH ? ORDER BY distance LIMIT 32.
|
||||
Байндить вектор тем же способом, что в тесте vec0KnnRoundTrip (bindVector).
|
||||
4. Слияние RRF: score(rowid) = сумма по спискам 1.0/(60.0 + rank), где rank — позиция в списке, начиная с 1.
|
||||
В режиме LEX — только лексический список, в VEC — только векторный, в HYBRID — оба.
|
||||
5. По итоговому score взять первые k rowid; вычитать из chunks поля path, line, heading, text
|
||||
(SELECT path, line, heading, text FROM chunks WHERE id IN (...)); сохранить порядок по score убыванию.
|
||||
6. text обрезать до 1200 символов (если длиннее — substring(0,1200) без многоточий).
|
||||
7. Пустой индекс → пустой список, без исключений.
|
||||
|
||||
## SearcherTest.kt — ровно 5 тестов, имена ровно такие
|
||||
|
||||
Подготовка (общий помощник): временный каталог + Db(index.db) + db.init() + Embedder(модель из MEMO_MODEL_DIR
|
||||
как в CoreSmokeTest.embedderProduces768). Помощник `addChunk(path, heading, line, text)`, который вставляет
|
||||
строку в chunks, берёт last_insert_rowid() и вставляет тот же rowid в chunks_fts(text, heading)
|
||||
и в chunks_vec(embedding) с эмбеддингом embedder.embed(text).
|
||||
|
||||
1. `lexModeFindsExactValue` — 3 чанка, в одном текст "Прокси корпоративных доменов на 76.132";
|
||||
поиск с mode = LEX по запросу "76.132" → первый хит содержит "76.132".
|
||||
2. `vecModeFindsSemanticMatch` — 3 чанка на разные темы (сервер/книга/CI);
|
||||
запрос про «настройку выпуска приложения», mode = VEC → первый хит — чанк про CI.
|
||||
3. `lexModeIgnoresVectorOnlyMatch` — запрос, не имеющий ни одного общего токена с чанками
|
||||
(например, при тексте чанков "опрос", "случай", "знание" запрос "автомобиль");
|
||||
mode = LEX → пустой список, mode = VEC → непустой.
|
||||
4. `hybridCombinesBoth` — в индексе чанк с точным значением и чанк со смысловым совпадением;
|
||||
запрос, попадающий в оба, mode = HYBRID, k = 5 → в результате присутствуют оба чанка.
|
||||
5. `resultsRespectKAndTextLength` — 4 чанка; k = 2 → ровно 2 хита; у каждого text непустой и его длина <= 1200.
|
||||
|
||||
После: ./gradlew :memo-core:test --rerun-tasks — все тесты зелёные.
|
||||
Коммит: git add -A && git commit -m "core: гибридный поиск (BM25 + вектор + RRF)"
|
||||
|
||||
СТРОГИЕ ЗАПРЕТЫ:
|
||||
- Не выводить план текстом; сразу создавай файлы.
|
||||
- Не трогать существующие файлы (Chunker.kt, Indexer.kt, Embedder.kt, Db.kt, CoreSmokeTest.kt, ChunkerTest.kt, build.gradle.kts).
|
||||
- Не добавлять зависимости.
|
||||
- Не создавать другие файлы.
|
||||
Reference in New Issue
Block a user