/* search-index.c * * Copyright 2026 Alexander Vanhee * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * SPDX-License-Identifier: GPL-3.0-or-later */ #include "config.h" #include "search-index.h" #include #include #include #include #include #define INDEX_MAGIC "BZSI" static int read_u32 (FILE *f, unsigned int *out); static char *read_field (FILE *f); static void free_entries (SearchIndexEntry *entries, unsigned int count); static double score_field (const char *term, const char *field, double weight); static double score_entry (const SearchIndexEntry *e, const char *const *terms, int n_terms); static int cmp_matches (const void *a, const void *b); SearchIndex * search_index_open (const char *path) { FILE *f = NULL; char magic[4] = { 0 }; unsigned int version = 0; unsigned int count = 0; SearchIndexEntry *entries = NULL; SearchIndex *idx = NULL; struct stat st = { 0 }; unsigned int i = 0; f = fopen (path, "rb"); if (f == NULL) return NULL; if (fread (magic, 1, 4, f) != 4 || memcmp (magic, INDEX_MAGIC, 4) != 0) { fclose (f); return NULL; } if (read_u32 (f, &version) < 0 || read_u32 (f, &count) < 0) { fclose (f); return NULL; } entries = calloc (count, sizeof (SearchIndexEntry)); if (entries == NULL) { fclose (f); return NULL; } for (i = 0; i < count; i++) { entries[i].id = read_field (f); entries[i].title = read_field (f); entries[i].developer = read_field (f); entries[i].description = read_field (f); entries[i].search_tokens = read_field (f); entries[i].icon_path = read_field (f); if (entries[i].id == NULL || entries[i].title == NULL) { free_entries (entries, count); fclose (f); return NULL; } } fclose (f); idx = calloc (1, sizeof (SearchIndex)); if (idx == NULL) { free_entries (entries, count); return NULL; } idx->path = strdup (path); idx->entries = entries; idx->count = count; if (stat (path, &st) == 0) idx->mtime = st.st_mtime; return idx; } void search_index_close (SearchIndex *idx) { if (idx == NULL) return; free_entries (idx->entries, idx->count); free (idx->path); free (idx); } int search_index_reload_if_stale (SearchIndex **idx_ptr) { struct stat st = { 0 }; SearchIndex *idx = NULL; SearchIndex *fresh = NULL; idx = *idx_ptr; if (idx == NULL) return 0; if (stat (idx->path, &st) != 0) return 0; if (st.st_mtime == idx->mtime) return 0; fresh = search_index_open (idx->path); if (fresh == NULL) return 0; search_index_close (idx); *idx_ptr = fresh; return 1; } size_t search_index_query (SearchIndex *idx, const char *const *terms, int n_terms, SearchIndexMatch *out, size_t max_results) { size_t n_matches = 0; unsigned int i = 0; double score = 0.0; if (idx == NULL || n_terms <= 0) return 0; for (i = 0; i < idx->count && n_matches < max_results * 8; i++) { score = score_entry (&idx->entries[i], terms, n_terms); if (score > 0.0 && n_matches < max_results * 8) { if (n_matches < max_results) { out[n_matches].entry = &idx->entries[i]; out[n_matches].score = score; } n_matches++; } } if (n_matches > max_results) n_matches = max_results; qsort (out, n_matches, sizeof (SearchIndexMatch), cmp_matches); return n_matches; } const SearchIndexEntry * search_index_find (SearchIndex *idx, const char *id) { unsigned int i = 0; if (idx == NULL || id == NULL) return NULL; for (i = 0; i < idx->count; i++) { if (strcmp (idx->entries[i].id, id) == 0) return &idx->entries[i]; } return NULL; } static int read_u32 (FILE *f, unsigned int *out) { unsigned char b[4] = { 0 }; if (fread (b, 1, 4, f) != 4) return -1; *out = (unsigned int) b[0] | ((unsigned int) b[1] << 8) | ((unsigned int) b[2] << 16) | ((unsigned int) b[3] << 24); return 0; } static char * read_field (FILE *f) { unsigned int len = 0; char *str = NULL; if (read_u32 (f, &len) < 0) return NULL; if (len == 0) return NULL; str = malloc (len + 1); if (str == NULL) return NULL; if (fread (str, 1, len, f) != len) { free (str); return NULL; } str[len] = '\0'; return str; } static void free_entries (SearchIndexEntry *entries, unsigned int count) { unsigned int i = 0; if (entries == NULL) return; for (i = 0; i < count; i++) { free (entries[i].id); free (entries[i].title); free (entries[i].developer); free (entries[i].description); free (entries[i].search_tokens); free (entries[i].icon_path); } free (entries); } static double score_field (const char *term, const char *field, double weight) { const char *hit = NULL; size_t term_len = 0; size_t field_len = 0; if (field == NULL || term == NULL || *term == '\0') return 0.0; hit = strcasestr (field, term); if (hit == NULL) return 0.0; term_len = strlen (term); field_len = strlen (field); if (field_len == 0) return 0.0; return weight * ((double) (term_len * term_len) / (double) field_len); } static double score_entry (const SearchIndexEntry *e, const char *const *terms, int n_terms) { double total = 0.0; int i = 0; const char *term = NULL; double term_score = 0.0; for (i = 0; i < n_terms; i++) { term = terms[i]; term_score = 0.0; if (term == NULL || *term == '\0') continue; term_score += score_field (term, e->title, 2.0); term_score += score_field (term, e->developer, 1.0); term_score += score_field (term, e->description, 1.0); term_score += score_field (term, e->search_tokens, 1.5); if (term_score <= 0.0) return 0.0; total += term_score; } return total; } static int cmp_matches (const void *a, const void *b) { const SearchIndexMatch *ma = a; const SearchIndexMatch *mb = b; if (mb->score > ma->score) return 1; if (mb->score < ma->score) return -1; return 0; }