feat: search all fields (#622)
* ✨ feat(search): search all feature - add Description field to Gist struct and index it - extend SearchGistMetadata with Description and Content - update Bleve and Meilisearch to index and search Description - modify ParseSearchQueryStr to parse description: and content: keywords - update templates and i18n for new search options * Fix test * Set content by default Signed-off-by: Thomas Miceli <tho.miceli@gmail.com> * Config to define default searchable fields Signed-off-by: Thomas Miceli <tho.miceli@gmail.com> --------- Signed-off-by: Thomas Miceli <tho.miceli@gmail.com> Co-authored-by: Thomas Miceli <tho.miceli@gmail.com>
هذا الالتزام موجود في:
ملتزم من قبل
GitHub
الأصل
5ad01a3304
التزام
279da52899
@@ -32,6 +32,10 @@ index.meili.host:
|
|||||||
# Set the API key for the Meiliseach server
|
# Set the API key for the Meiliseach server
|
||||||
index.meili.api-key:
|
index.meili.api-key:
|
||||||
|
|
||||||
|
# Set the default search fields. Can contain multiple fields (e.g., `content,username`).
|
||||||
|
# Fields: content,user,title,description,filename,extension,language,topic. Default: content
|
||||||
|
search.default: content
|
||||||
|
|
||||||
# Default branch name used by Opengist when initializing Git repositories.
|
# Default branch name used by Opengist when initializing Git repositories.
|
||||||
# If not set, uses the Git default branch name. See https://git-scm.com/book/en/v2/Getting-Started-First-Time-Git-Setup#_new_default_branch
|
# If not set, uses the Git default branch name. See https://git-scm.com/book/en/v2/Getting-Started-First-Time-Git-Setup#_new_default_branch
|
||||||
git.default-branch:
|
git.default-branch:
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ aside: false
|
|||||||
| index | OG_INDEX | `bleve` | Define the code indexer (either `bleve`, `meilisearch`, or empty for no index). |
|
| index | OG_INDEX | `bleve` | Define the code indexer (either `bleve`, `meilisearch`, or empty for no index). |
|
||||||
| index.meili.host | OG_MEILI_HOST | none | Set the host for the Meiliseach server. |
|
| index.meili.host | OG_MEILI_HOST | none | Set the host for the Meiliseach server. |
|
||||||
| index.meili.api-key | OG_MEILI_API_KEY | none | Set the API key for the Meiliseach server. |
|
| index.meili.api-key | OG_MEILI_API_KEY | none | Set the API key for the Meiliseach server. |
|
||||||
|
| search.default | OG_SEARCH_DEFAULT | `content` | Set the default search fields. Can contain multiple fields (e.g., `content,username`). Fields: `content,user,title,description,filename,extension,language,topic`. |
|
||||||
| git.default-branch | OG_GIT_DEFAULT_BRANCH | none | Default branch name used by Opengist when initializing Git repositories. If not set, uses the Git default branch name. More info [here](https://git-scm.com/book/en/v2/Getting-Started-First-Time-Git-Setup#_new_default_branch) |
|
| git.default-branch | OG_GIT_DEFAULT_BRANCH | none | Default branch name used by Opengist when initializing Git repositories. If not set, uses the Git default branch name. More info [here](https://git-scm.com/book/en/v2/Getting-Started-First-Time-Git-Setup#_new_default_branch) |
|
||||||
| sqlite.journal-mode | OG_SQLITE_JOURNAL_MODE | `WAL` | Set the journal mode for SQLite. More info [here](https://www.sqlite.org/pragma.html#pragma_journal_mode) |
|
| sqlite.journal-mode | OG_SQLITE_JOURNAL_MODE | `WAL` | Set the journal mode for SQLite. More info [here](https://www.sqlite.org/pragma.html#pragma_journal_mode) |
|
||||||
| http.host | OG_HTTP_HOST | `0.0.0.0` | The host on which the HTTP server should bind. Use an IP address for network binding. Use a path for Unix socket binding (e.g. /run/opengist.sock) |
|
| http.host | OG_HTTP_HOST | `0.0.0.0` | The host on which the HTTP server should bind. Use an IP address for network binding. Use a path for Unix socket binding (e.g. /run/opengist.sock) |
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ type config struct {
|
|||||||
BleveDirname string `yaml:"index.dirname" env:"OG_INDEX_DIRNAME"` // deprecated
|
BleveDirname string `yaml:"index.dirname" env:"OG_INDEX_DIRNAME"` // deprecated
|
||||||
MeiliHost string `yaml:"index.meili.host" env:"OG_MEILI_HOST"`
|
MeiliHost string `yaml:"index.meili.host" env:"OG_MEILI_HOST"`
|
||||||
MeiliAPIKey string `yaml:"index.meili.api-key" env:"OG_MEILI_API_KEY"`
|
MeiliAPIKey string `yaml:"index.meili.api-key" env:"OG_MEILI_API_KEY"`
|
||||||
|
SearchDefault string `yaml:"search.default" env:"OG_SEARCH_DEFAULT"`
|
||||||
|
|
||||||
GitDefaultBranch string `yaml:"git.default-branch" env:"OG_GIT_DEFAULT_BRANCH"`
|
GitDefaultBranch string `yaml:"git.default-branch" env:"OG_GIT_DEFAULT_BRANCH"`
|
||||||
|
|
||||||
@@ -111,6 +112,7 @@ func configWithDefaults() (*config, error) {
|
|||||||
c.OpengistHome = ""
|
c.OpengistHome = ""
|
||||||
c.DBUri = "opengist.db"
|
c.DBUri = "opengist.db"
|
||||||
c.Index = "bleve"
|
c.Index = "bleve"
|
||||||
|
c.SearchDefault = "content"
|
||||||
|
|
||||||
c.SqliteJournalMode = "WAL"
|
c.SqliteJournalMode = "WAL"
|
||||||
|
|
||||||
|
|||||||
@@ -821,6 +821,7 @@ func (gist *Gist) ToIndexedGist() (*index.Gist, error) {
|
|||||||
UserID: gist.UserID,
|
UserID: gist.UserID,
|
||||||
Visibility: gist.Private.Uint(),
|
Visibility: gist.Private.Uint(),
|
||||||
Username: gist.User.Username,
|
Username: gist.User.Username,
|
||||||
|
Description: gist.Description,
|
||||||
Title: gist.Title,
|
Title: gist.Title,
|
||||||
Content: wholeContent,
|
Content: wholeContent,
|
||||||
Filenames: fileNames,
|
Filenames: fileNames,
|
||||||
|
|||||||
@@ -88,10 +88,12 @@ gist.search.found: gists found
|
|||||||
gist.search.no-results: No gists found
|
gist.search.no-results: No gists found
|
||||||
gist.search.help.user: gists created by user
|
gist.search.help.user: gists created by user
|
||||||
gist.search.help.title: gists with given title
|
gist.search.help.title: gists with given title
|
||||||
|
gist.search.help.description: gists with given description
|
||||||
gist.search.help.filename: gists having files with given name
|
gist.search.help.filename: gists having files with given name
|
||||||
gist.search.help.extension: gists having files with given extension
|
gist.search.help.extension: gists having files with given extension
|
||||||
gist.search.help.language: gists having files with given language
|
gist.search.help.language: gists having files with given language
|
||||||
gist.search.help.topic: gists with given topic
|
gist.search.help.topic: gists with given topic
|
||||||
|
gist.search.help.all: search all fields
|
||||||
gist.search.placeholder.title: Title
|
gist.search.placeholder.title: Title
|
||||||
gist.search.placeholder.visibility: Visibility
|
gist.search.placeholder.visibility: Visibility
|
||||||
gist.search.placeholder.public: Public
|
gist.search.placeholder.public: Public
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/blevesearch/bleve/v2"
|
"github.com/blevesearch/bleve/v2"
|
||||||
"github.com/blevesearch/bleve/v2/analysis/analyzer/custom"
|
"github.com/blevesearch/bleve/v2/analysis/analyzer/custom"
|
||||||
@@ -14,6 +15,7 @@ import (
|
|||||||
"github.com/blevesearch/bleve/v2/analysis/tokenizer/unicode"
|
"github.com/blevesearch/bleve/v2/analysis/tokenizer/unicode"
|
||||||
"github.com/blevesearch/bleve/v2/search/query"
|
"github.com/blevesearch/bleve/v2/search/query"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
|
"github.com/thomiceli/opengist/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
type BleveIndexer struct {
|
type BleveIndexer struct {
|
||||||
@@ -116,18 +118,60 @@ func (i *BleveIndexer) Remove(gistID uint) error {
|
|||||||
return (*atomicIndexer.Load()).(*BleveIndexer).index.Delete(strconv.Itoa(int(gistID)))
|
return (*atomicIndexer.Load()).(*BleveIndexer).index.Delete(strconv.Itoa(int(gistID)))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *BleveIndexer) Search(queryStr string, queryMetadata SearchGistMetadata, userId uint, page int) ([]uint, uint64, map[string]int, error) {
|
// Search returns a list of Gist IDs that match the given search metadata.
|
||||||
|
// The method returns an error if any.
|
||||||
|
//
|
||||||
|
// The queryMetadata parameter is used to filter the search results.
|
||||||
|
// For example, passing a non-empty Username will search for gists whose
|
||||||
|
// username matches the given string.
|
||||||
|
//
|
||||||
|
// If the "All" field in queryMetadata is non-empty, the method will
|
||||||
|
// search across all metadata fields with OR logic. Otherwise, the method
|
||||||
|
// will add each metadata field with AND logic.
|
||||||
|
//
|
||||||
|
// The page parameter is used to paginate the search results.
|
||||||
|
// The method returns the total number of search results in the second return
|
||||||
|
// value.
|
||||||
|
//
|
||||||
|
// The third return value is a map of language counts for the search results.
|
||||||
|
// The language counts are computed by asking Bleve to return the top 10
|
||||||
|
// facets for the "Languages" field.
|
||||||
|
func (i *BleveIndexer) Search(metadata SearchGistMetadata, userId uint, page int) ([]uint, uint64, map[string]int, error) {
|
||||||
var err error
|
var err error
|
||||||
var indexerQuery query.Query
|
var indexerQuery query.Query = bleve.NewMatchAllQuery()
|
||||||
if queryStr != "" {
|
|
||||||
// Use match query with fuzzy matching for more flexible content search
|
// Query factory
|
||||||
contentQuery := bleve.NewMatchQuery(queryStr)
|
factoryQuery := func(field, value string) query.Query {
|
||||||
contentQuery.SetField("Content")
|
query := bleve.NewMatchPhraseQuery(value)
|
||||||
contentQuery.SetFuzziness(2)
|
query.SetField(field)
|
||||||
indexerQuery = contentQuery
|
return query
|
||||||
} else {
|
}
|
||||||
contentQuery := bleve.NewMatchAllQuery()
|
|
||||||
indexerQuery = contentQuery
|
// Exact search
|
||||||
|
addQuery := func(field, value string) {
|
||||||
|
if value != "" && value != "." {
|
||||||
|
indexerQuery = bleve.NewConjunctionQuery(indexerQuery, factoryQuery(field, value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exact+fuzzy query factory: exact match is boosted so it ranks above fuzzy-only matches
|
||||||
|
factoryFuzzyQuery := func(field, value string) query.Query {
|
||||||
|
exact := bleve.NewMatchPhraseQuery(value)
|
||||||
|
exact.SetField(field)
|
||||||
|
exact.SetBoost(2.0)
|
||||||
|
|
||||||
|
fuzzy := bleve.NewMatchQuery(value)
|
||||||
|
fuzzy.SetField(field)
|
||||||
|
fuzzy.SetFuzziness(2)
|
||||||
|
|
||||||
|
return bleve.NewDisjunctionQuery(exact, fuzzy)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exact+fuzzy search
|
||||||
|
addFuzzy := func(field, value string) {
|
||||||
|
if value != "" && value != "." {
|
||||||
|
indexerQuery = bleve.NewConjunctionQuery(indexerQuery, factoryFuzzyQuery(field, value))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Visibility filtering: show public gists (Visibility=0) OR user's own gists
|
// Visibility filtering: show public gists (Visibility=0) OR user's own gists
|
||||||
@@ -143,48 +187,58 @@ func (i *BleveIndexer) Search(queryStr string, queryMetadata SearchGistMetadata,
|
|||||||
accessQuery := bleve.NewDisjunctionQuery(publicQuery, userIdQuery)
|
accessQuery := bleve.NewDisjunctionQuery(publicQuery, userIdQuery)
|
||||||
indexerQuery = bleve.NewConjunctionQuery(accessQuery, indexerQuery)
|
indexerQuery = bleve.NewConjunctionQuery(accessQuery, indexerQuery)
|
||||||
|
|
||||||
|
buildFieldQuery := func(field, value string) query.Query {
|
||||||
|
switch field {
|
||||||
|
case "Title", "Description", "Filenames", "Content":
|
||||||
|
return factoryFuzzyQuery(field, value)
|
||||||
|
case "Extensions":
|
||||||
|
return factoryQuery(field, "."+value)
|
||||||
|
default: // Username, Languages, Topics
|
||||||
|
return factoryQuery(field, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Handle "All" field - search across all metadata fields with OR logic
|
// Handle "All" field - search across all metadata fields with OR logic
|
||||||
if queryMetadata.All != "" {
|
if metadata.All != "" {
|
||||||
allQueries := make([]query.Query, 0)
|
allQueries := make([]query.Query, 0, len(AllSearchFields))
|
||||||
|
for _, field := range AllSearchFields {
|
||||||
// Create match phrase queries for each field
|
allQueries = append(allQueries, buildFieldQuery(field, metadata.All))
|
||||||
fields := []struct {
|
|
||||||
field string
|
|
||||||
value string
|
|
||||||
}{
|
|
||||||
{"Username", queryMetadata.All},
|
|
||||||
{"Title", queryMetadata.All},
|
|
||||||
{"Extensions", "." + queryMetadata.All},
|
|
||||||
{"Filenames", queryMetadata.All},
|
|
||||||
{"Languages", queryMetadata.All},
|
|
||||||
{"Topics", queryMetadata.All},
|
|
||||||
}
|
}
|
||||||
|
indexerQuery = bleve.NewConjunctionQuery(indexerQuery, bleve.NewDisjunctionQuery(allQueries...))
|
||||||
for _, f := range fields {
|
|
||||||
q := bleve.NewMatchPhraseQuery(f.value)
|
|
||||||
q.FieldVal = f.field
|
|
||||||
allQueries = append(allQueries, q)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Combine all field queries with OR (disjunction)
|
|
||||||
allDisjunction := bleve.NewDisjunctionQuery(allQueries...)
|
|
||||||
indexerQuery = bleve.NewConjunctionQuery(indexerQuery, allDisjunction)
|
|
||||||
} else {
|
} else {
|
||||||
// Original behavior: add each metadata field with AND logic
|
// Original behavior: add each metadata field with AND logic
|
||||||
addQuery := func(field, value string) {
|
addQuery("Username", metadata.Username)
|
||||||
if value != "" && value != "." {
|
addFuzzy("Title", metadata.Title)
|
||||||
q := bleve.NewMatchPhraseQuery(value)
|
addFuzzy("Description", metadata.Description)
|
||||||
q.FieldVal = field
|
addQuery("Extensions", "."+metadata.Extension)
|
||||||
indexerQuery = bleve.NewConjunctionQuery(indexerQuery, q)
|
addFuzzy("Filenames", metadata.Filename)
|
||||||
}
|
addQuery("Languages", metadata.Language)
|
||||||
}
|
addQuery("Topics", metadata.Topic)
|
||||||
|
addFuzzy("Content", metadata.Content)
|
||||||
|
|
||||||
addQuery("Username", queryMetadata.Username)
|
// Handle default search fields from config with OR logic
|
||||||
addQuery("Title", queryMetadata.Title)
|
if metadata.Default != "" {
|
||||||
addQuery("Extensions", "."+queryMetadata.Extension)
|
var fields []string
|
||||||
addQuery("Filenames", queryMetadata.Filename)
|
for _, f := range strings.Split(config.C.SearchDefault, ",") {
|
||||||
addQuery("Languages", queryMetadata.Language)
|
f = strings.TrimSpace(f)
|
||||||
addQuery("Topics", queryMetadata.Topic)
|
if f == "all" {
|
||||||
|
fields = AllSearchFields
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if indexField, ok := SearchFieldMap[f]; ok {
|
||||||
|
fields = append(fields, indexField)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(fields) == 1 {
|
||||||
|
indexerQuery = bleve.NewConjunctionQuery(indexerQuery, buildFieldQuery(fields[0], metadata.Default))
|
||||||
|
} else if len(fields) > 1 {
|
||||||
|
defaultQueries := make([]query.Query, 0, len(fields))
|
||||||
|
for _, field := range fields {
|
||||||
|
defaultQueries = append(defaultQueries, buildFieldQuery(field, metadata.Default))
|
||||||
|
}
|
||||||
|
indexerQuery = bleve.NewConjunctionQuery(indexerQuery, bleve.NewDisjunctionQuery(defaultQueries...))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
languageFacet := bleve.NewFacetRequest("Languages", 10)
|
languageFacet := bleve.NewFacetRequest("Languages", 10)
|
||||||
@@ -197,6 +251,8 @@ func (i *BleveIndexer) Search(queryStr string, queryMetadata SearchGistMetadata,
|
|||||||
s.Fields = []string{"GistID"}
|
s.Fields = []string{"GistID"}
|
||||||
s.IncludeLocations = false
|
s.IncludeLocations = false
|
||||||
|
|
||||||
|
log.Debug().Interface("searchRequest", s).Msg("Bleve search request")
|
||||||
|
|
||||||
results, err := (*atomicIndexer.Load()).(*BleveIndexer).index.Search(s)
|
results, err := (*atomicIndexer.Load()).(*BleveIndexer).index.Search(s)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, nil, err
|
return nil, 0, nil, err
|
||||||
|
|||||||
@@ -124,6 +124,7 @@ func TestBleveIndexerUnicodeSearch(t *testing.T) {
|
|||||||
Visibility: 0,
|
Visibility: 0,
|
||||||
Username: "testuser",
|
Username: "testuser",
|
||||||
Title: "Unicode Test",
|
Title: "Unicode Test",
|
||||||
|
Description: "Descrition with Unicode characters: Café résumé naive",
|
||||||
Content: "Hello world with unicode characters: café résumé naïve",
|
Content: "Hello world with unicode characters: café résumé naïve",
|
||||||
Filenames: []string{"test.txt"},
|
Filenames: []string{"test.txt"},
|
||||||
Extensions: []string{".txt"},
|
Extensions: []string{".txt"},
|
||||||
@@ -139,7 +140,7 @@ func TestBleveIndexerUnicodeSearch(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Search for unicode content
|
// Search for unicode content
|
||||||
gistIDs, total, _, err := indexer.Search("café", SearchGistMetadata{}, 100, 1)
|
gistIDs, total, _, err := indexer.Search(SearchGistMetadata{All: "café"}, 100, 1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Search failed: %v", err)
|
t.Fatalf("Search failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,24 @@
|
|||||||
package index
|
package index
|
||||||
|
|
||||||
|
var AllSearchFields = []string{"Username", "Title", "Description", "Filenames", "Extensions", "Languages", "Topics", "Content"}
|
||||||
|
|
||||||
|
var SearchFieldMap = map[string]string{
|
||||||
|
"user": "Username",
|
||||||
|
"title": "Title",
|
||||||
|
"description": "Description",
|
||||||
|
"filename": "Filenames",
|
||||||
|
"extension": "Extensions",
|
||||||
|
"language": "Languages",
|
||||||
|
"topic": "Topics",
|
||||||
|
"content": "Content",
|
||||||
|
}
|
||||||
|
|
||||||
type Gist struct {
|
type Gist struct {
|
||||||
GistID uint
|
GistID uint
|
||||||
UserID uint
|
UserID uint
|
||||||
Visibility uint
|
Visibility uint
|
||||||
Username string
|
Username string
|
||||||
|
Description string
|
||||||
Title string
|
Title string
|
||||||
Content string
|
Content string
|
||||||
Filenames []string
|
Filenames []string
|
||||||
@@ -18,9 +32,12 @@ type Gist struct {
|
|||||||
type SearchGistMetadata struct {
|
type SearchGistMetadata struct {
|
||||||
Username string
|
Username string
|
||||||
Title string
|
Title string
|
||||||
|
Description string
|
||||||
|
Content string
|
||||||
Filename string
|
Filename string
|
||||||
Extension string
|
Extension string
|
||||||
Language string
|
Language string
|
||||||
Topic string
|
Topic string
|
||||||
All string
|
All string
|
||||||
|
Default string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ type Indexer interface {
|
|||||||
Reset() error
|
Reset() error
|
||||||
Add(gist *Gist) error
|
Add(gist *Gist) error
|
||||||
Remove(gistID uint) error
|
Remove(gistID uint) error
|
||||||
Search(query string, metadata SearchGistMetadata, userId uint, page int) ([]uint, uint64, map[string]int, error)
|
Search(metadata SearchGistMetadata, userId uint, page int) ([]uint, uint64, map[string]int, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type IndexerType string
|
type IndexerType string
|
||||||
@@ -125,7 +125,11 @@ func RemoveFromIndex(gistID uint) error {
|
|||||||
return (*idx).Remove(gistID)
|
return (*idx).Remove(gistID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func SearchGists(query string, metadata SearchGistMetadata, userId uint, page int) ([]uint, uint64, map[string]int, error) {
|
// SearchGists returns a list of Gist IDs that match the given search metadata.
|
||||||
|
// If the indexer is not enabled, it returns nil, 0, nil, nil.
|
||||||
|
// If the indexer is not initialized, it returns nil, 0, nil, fmt.Errorf("indexer is not initialized").
|
||||||
|
// The function returns an error if any.
|
||||||
|
func SearchGists(metadata SearchGistMetadata, userId uint, page int) ([]uint, uint64, map[string]int, error) {
|
||||||
if !IndexEnabled() {
|
if !IndexEnabled() {
|
||||||
return nil, 0, nil, nil
|
return nil, 0, nil, nil
|
||||||
}
|
}
|
||||||
@@ -135,7 +139,7 @@ func SearchGists(query string, metadata SearchGistMetadata, userId uint, page in
|
|||||||
return nil, 0, nil, fmt.Errorf("indexer is not initialized")
|
return nil, 0, nil, fmt.Errorf("indexer is not initialized")
|
||||||
}
|
}
|
||||||
|
|
||||||
return (*idx).Search(query, metadata, userId, page)
|
return (*idx).Search(metadata, userId, page)
|
||||||
}
|
}
|
||||||
|
|
||||||
func DepreactionIndexDirname() {
|
func DepreactionIndexDirname() {
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ func testIndexerAddGist(t *testing.T, indexer Indexer) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Verify gist is searchable
|
// Verify gist is searchable
|
||||||
gistIDs, total, _, err := indexer.Search("test gist", SearchGistMetadata{}, 11, 1)
|
gistIDs, total, _, err := indexer.Search(SearchGistMetadata{All: "test gist"}, 11, 1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Search failed: %v", err)
|
t.Fatalf("Search failed: %v", err)
|
||||||
}
|
}
|
||||||
@@ -114,7 +114,7 @@ func testIndexerAddGist(t *testing.T, indexer Indexer) {
|
|||||||
|
|
||||||
// Search by Rust language
|
// Search by Rust language
|
||||||
metadata := SearchGistMetadata{Language: "Rust"}
|
metadata := SearchGistMetadata{Language: "Rust"}
|
||||||
gistIDs, total, _, err := indexer.Search("", metadata, 11, 1)
|
gistIDs, total, _, err := indexer.Search(metadata, 11, 1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Search by Rust language failed: %v", err)
|
t.Fatalf("Search by Rust language failed: %v", err)
|
||||||
}
|
}
|
||||||
@@ -156,7 +156,7 @@ func testIndexerAddGist(t *testing.T, indexer Indexer) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Search for unicode content
|
// Search for unicode content
|
||||||
_, total, _, err := indexer.Search("café", SearchGistMetadata{}, 11, 1)
|
_, total, _, err := indexer.Search(SearchGistMetadata{All: "café"}, 11, 1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Search for unicode failed: %v", err)
|
t.Fatalf("Search for unicode failed: %v", err)
|
||||||
}
|
}
|
||||||
@@ -189,7 +189,7 @@ func testIndexerAddGist(t *testing.T, indexer Indexer) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// User 11 should see their own private gist
|
// User 11 should see their own private gist
|
||||||
gistIDs, total, _, err := indexer.Search("private gist", SearchGistMetadata{}, 11, 1)
|
gistIDs, total, _, err := indexer.Search(SearchGistMetadata{All: "private gist"}, 11, 1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Search for private gist as owner failed: %v", err)
|
t.Fatalf("Search for private gist as owner failed: %v", err)
|
||||||
}
|
}
|
||||||
@@ -205,7 +205,7 @@ func testIndexerAddGist(t *testing.T, indexer Indexer) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// User 999 should NOT see user 11's private gist
|
// User 999 should NOT see user 11's private gist
|
||||||
gistIDs2, _, _, err := indexer.Search("private gist", SearchGistMetadata{}, 999, 1)
|
gistIDs2, _, _, err := indexer.Search(SearchGistMetadata{All: "private gist"}, 999, 1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Search for private gist as other user failed: %v", err)
|
t.Fatalf("Search for private gist as other user failed: %v", err)
|
||||||
}
|
}
|
||||||
@@ -239,7 +239,7 @@ func testIndexerAddGist(t *testing.T, indexer Indexer) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Should still be searchable by content
|
// Should still be searchable by content
|
||||||
_, total, _, err := indexer.Search("Minimal", SearchGistMetadata{}, 11, 1)
|
_, total, _, err := indexer.Search(SearchGistMetadata{All: "Minimal"}, 11, 1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Search for minimal gist failed: %v", err)
|
t.Fatalf("Search for minimal gist failed: %v", err)
|
||||||
}
|
}
|
||||||
@@ -292,7 +292,7 @@ func testIndexerAddGist(t *testing.T, indexer Indexer) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Search should find updated content, not original
|
// Search should find updated content, not original
|
||||||
gistIDs, total, _, err := indexer.Search("new information", SearchGistMetadata{}, 11, 1)
|
gistIDs, total, _, err := indexer.Search(SearchGistMetadata{All: "new information"}, 11, 1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Search for updated content failed: %v", err)
|
t.Fatalf("Search for updated content failed: %v", err)
|
||||||
}
|
}
|
||||||
@@ -308,7 +308,7 @@ func testIndexerAddGist(t *testing.T, indexer Indexer) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Old content should not be found
|
// Old content should not be found
|
||||||
gistIDsOld, _, _, _ := indexer.Search("Original", SearchGistMetadata{}, 11, 1)
|
gistIDsOld, _, _, _ := indexer.Search(SearchGistMetadata{All: "Original"}, 11, 1)
|
||||||
for _, id := range gistIDsOld {
|
for _, id := range gistIDsOld {
|
||||||
if id == 1006 {
|
if id == 1006 {
|
||||||
t.Error("Should not find gist by old content after update")
|
t.Error("Should not find gist by old content after update")
|
||||||
@@ -340,7 +340,7 @@ func testIndexerAddGist(t *testing.T, indexer Indexer) {
|
|||||||
|
|
||||||
// Search by username
|
// Search by username
|
||||||
metadata := SearchGistMetadata{Username: "newuser"}
|
metadata := SearchGistMetadata{Username: "newuser"}
|
||||||
gistIDs, total, _, err := indexer.Search("", metadata, 12, 1)
|
gistIDs, total, _, err := indexer.Search(metadata, 12, 1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Search by username failed: %v", err)
|
t.Fatalf("Search by username failed: %v", err)
|
||||||
}
|
}
|
||||||
@@ -383,7 +383,7 @@ func testIndexerAddGist(t *testing.T, indexer Indexer) {
|
|||||||
|
|
||||||
// Search by one of the topics
|
// Search by one of the topics
|
||||||
metadata := SearchGistMetadata{Topic: "microservices"}
|
metadata := SearchGistMetadata{Topic: "microservices"}
|
||||||
gistIDs, total, _, err := indexer.Search("", metadata, 11, 1)
|
gistIDs, total, _, err := indexer.Search(metadata, 11, 1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Search by topic failed: %v", err)
|
t.Fatalf("Search by topic failed: %v", err)
|
||||||
}
|
}
|
||||||
@@ -427,7 +427,7 @@ func testIndexerAddGist(t *testing.T, indexer Indexer) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Search for content from the middle
|
// Search for content from the middle
|
||||||
gistIDs, total, _, err := indexer.Search("Line 500", SearchGistMetadata{}, 11, 1)
|
gistIDs, total, _, err := indexer.Search(SearchGistMetadata{All: "Line 500"}, 11, 1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Search in long content failed: %v", err)
|
t.Fatalf("Search in long content failed: %v", err)
|
||||||
}
|
}
|
||||||
@@ -451,7 +451,7 @@ func testIndexerSearchBasic(t *testing.T, indexer Indexer) {
|
|||||||
|
|
||||||
// Test 1: Search by content - all init gists have "searchable content"
|
// Test 1: Search by content - all init gists have "searchable content"
|
||||||
t.Run("SearchByContent", func(t *testing.T) {
|
t.Run("SearchByContent", func(t *testing.T) {
|
||||||
gistIDs, total, languageCounts, err := indexer.Search("searchable", SearchGistMetadata{}, 1, 1)
|
gistIDs, total, languageCounts, err := indexer.Search(SearchGistMetadata{All: "searchable"}, 1, 1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Search failed: %v", err)
|
t.Fatalf("Search failed: %v", err)
|
||||||
}
|
}
|
||||||
@@ -475,7 +475,7 @@ func testIndexerSearchBasic(t *testing.T, indexer Indexer) {
|
|||||||
// Test 2: Search by specific language - Go
|
// Test 2: Search by specific language - Go
|
||||||
t.Run("SearchByLanguage", func(t *testing.T) {
|
t.Run("SearchByLanguage", func(t *testing.T) {
|
||||||
metadata := SearchGistMetadata{Language: "Go"}
|
metadata := SearchGistMetadata{Language: "Go"}
|
||||||
_, total, _, err := indexer.Search("", metadata, 1, 1)
|
_, total, _, err := indexer.Search(metadata, 1, 1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Search by language failed: %v", err)
|
t.Fatalf("Search by language failed: %v", err)
|
||||||
}
|
}
|
||||||
@@ -489,7 +489,7 @@ func testIndexerSearchBasic(t *testing.T, indexer Indexer) {
|
|||||||
// Test 3: Search by specific username - alice
|
// Test 3: Search by specific username - alice
|
||||||
t.Run("SearchByUsername", func(t *testing.T) {
|
t.Run("SearchByUsername", func(t *testing.T) {
|
||||||
metadata := SearchGistMetadata{Username: "alice"}
|
metadata := SearchGistMetadata{Username: "alice"}
|
||||||
_, total, _, err := indexer.Search("", metadata, 1, 1)
|
_, total, _, err := indexer.Search(metadata, 1, 1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Search by username failed: %v", err)
|
t.Fatalf("Search by username failed: %v", err)
|
||||||
}
|
}
|
||||||
@@ -503,7 +503,7 @@ func testIndexerSearchBasic(t *testing.T, indexer Indexer) {
|
|||||||
// Test 4: Search by extension - Python (bob's private files)
|
// Test 4: Search by extension - Python (bob's private files)
|
||||||
t.Run("SearchByExtension", func(t *testing.T) {
|
t.Run("SearchByExtension", func(t *testing.T) {
|
||||||
metadata := SearchGistMetadata{Extension: "py"}
|
metadata := SearchGistMetadata{Extension: "py"}
|
||||||
_, total, _, err := indexer.Search("", metadata, 1, 1)
|
_, total, _, err := indexer.Search(metadata, 1, 1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Search by extension failed: %v", err)
|
t.Fatalf("Search by extension failed: %v", err)
|
||||||
}
|
}
|
||||||
@@ -518,7 +518,7 @@ func testIndexerSearchBasic(t *testing.T, indexer Indexer) {
|
|||||||
// Test 5: Search by topic - algorithms
|
// Test 5: Search by topic - algorithms
|
||||||
t.Run("SearchByTopic", func(t *testing.T) {
|
t.Run("SearchByTopic", func(t *testing.T) {
|
||||||
metadata := SearchGistMetadata{Topic: "algorithms"}
|
metadata := SearchGistMetadata{Topic: "algorithms"}
|
||||||
_, total, _, err := indexer.Search("", metadata, 1, 1)
|
_, total, _, err := indexer.Search(metadata, 1, 1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Search by topic failed: %v", err)
|
t.Fatalf("Search by topic failed: %v", err)
|
||||||
}
|
}
|
||||||
@@ -535,7 +535,7 @@ func testIndexerSearchBasic(t *testing.T, indexer Indexer) {
|
|||||||
Language: "Go",
|
Language: "Go",
|
||||||
Username: "alice",
|
Username: "alice",
|
||||||
}
|
}
|
||||||
_, total, _, err := indexer.Search("", metadata, 1, 1)
|
_, total, _, err := indexer.Search(metadata, 1, 1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Search with combined filters failed: %v", err)
|
t.Fatalf("Search with combined filters failed: %v", err)
|
||||||
}
|
}
|
||||||
@@ -548,7 +548,7 @@ func testIndexerSearchBasic(t *testing.T, indexer Indexer) {
|
|||||||
|
|
||||||
// Test 7: Search with no results
|
// Test 7: Search with no results
|
||||||
t.Run("SearchNoResults", func(t *testing.T) {
|
t.Run("SearchNoResults", func(t *testing.T) {
|
||||||
gistIDs, total, _, err := indexer.Search("nonexistentquery", SearchGistMetadata{}, 1, 1)
|
gistIDs, total, _, err := indexer.Search(SearchGistMetadata{All: "nonexistentquery"}, 1, 1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Search with no results failed: %v", err)
|
t.Fatalf("Search with no results failed: %v", err)
|
||||||
}
|
}
|
||||||
@@ -562,7 +562,7 @@ func testIndexerSearchBasic(t *testing.T, indexer Indexer) {
|
|||||||
|
|
||||||
// Test 8: Empty query returns all accessible gists
|
// Test 8: Empty query returns all accessible gists
|
||||||
t.Run("SearchEmptyQuery", func(t *testing.T) {
|
t.Run("SearchEmptyQuery", func(t *testing.T) {
|
||||||
gistIDs, total, languageCounts, err := indexer.Search("", SearchGistMetadata{}, 1, 1)
|
gistIDs, total, languageCounts, err := indexer.Search(SearchGistMetadata{}, 1, 1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Empty search failed: %v", err)
|
t.Fatalf("Empty search failed: %v", err)
|
||||||
}
|
}
|
||||||
@@ -586,7 +586,7 @@ func testIndexerSearchBasic(t *testing.T, indexer Indexer) {
|
|||||||
t.Run("SearchPagination", func(t *testing.T) {
|
t.Run("SearchPagination", func(t *testing.T) {
|
||||||
// As user 1, we have 334 gists total
|
// As user 1, we have 334 gists total
|
||||||
// Page 1
|
// Page 1
|
||||||
gistIDs1, total, _, err := indexer.Search("searchable", SearchGistMetadata{}, 1, 1)
|
gistIDs1, total, _, err := indexer.Search(SearchGistMetadata{All: "searchable"}, 1, 1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Page 1 search failed: %v", err)
|
t.Fatalf("Page 1 search failed: %v", err)
|
||||||
}
|
}
|
||||||
@@ -598,7 +598,7 @@ func testIndexerSearchBasic(t *testing.T, indexer Indexer) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Page 2
|
// Page 2
|
||||||
gistIDs2, _, _, err := indexer.Search("searchable", SearchGistMetadata{}, 1, 2)
|
gistIDs2, _, _, err := indexer.Search(SearchGistMetadata{All: "searchable"}, 1, 2)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Page 2 search failed: %v", err)
|
t.Fatalf("Page 2 search failed: %v", err)
|
||||||
}
|
}
|
||||||
@@ -619,7 +619,7 @@ func testIndexerSearchBasic(t *testing.T, indexer Indexer) {
|
|||||||
// Search as user 2 (bob)
|
// Search as user 2 (bob)
|
||||||
// bob has 333 gists at i=1,4,7,... with visibility=1 (private)
|
// bob has 333 gists at i=1,4,7,... with visibility=1 (private)
|
||||||
// As user 2, bob sees: alice's 334 public gists + bob's own 333 gists = 667 total
|
// As user 2, bob sees: alice's 334 public gists + bob's own 333 gists = 667 total
|
||||||
_, total, _, err := indexer.Search("", SearchGistMetadata{}, 2, 1)
|
_, total, _, err := indexer.Search(SearchGistMetadata{}, 2, 1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Search as user 2 failed: %v", err)
|
t.Fatalf("Search as user 2 failed: %v", err)
|
||||||
}
|
}
|
||||||
@@ -628,7 +628,7 @@ func testIndexerSearchBasic(t *testing.T, indexer Indexer) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Search as non-existent user (should only see public gists)
|
// Search as non-existent user (should only see public gists)
|
||||||
_, totalPublic, _, err := indexer.Search("", SearchGistMetadata{}, 999, 1)
|
_, totalPublic, _, err := indexer.Search(SearchGistMetadata{}, 999, 1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Search as user 999 failed: %v", err)
|
t.Fatalf("Search as user 999 failed: %v", err)
|
||||||
}
|
}
|
||||||
@@ -645,7 +645,7 @@ func testIndexerSearchBasic(t *testing.T, indexer Indexer) {
|
|||||||
|
|
||||||
// Test 11: Language facets validation
|
// Test 11: Language facets validation
|
||||||
t.Run("LanguageFacets", func(t *testing.T) {
|
t.Run("LanguageFacets", func(t *testing.T) {
|
||||||
_, _, languageCounts, err := indexer.Search("", SearchGistMetadata{}, 1, 1)
|
_, _, languageCounts, err := indexer.Search(SearchGistMetadata{}, 1, 1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Search for facets failed: %v", err)
|
t.Fatalf("Search for facets failed: %v", err)
|
||||||
}
|
}
|
||||||
@@ -755,7 +755,7 @@ func testIndexerAllFieldSearch(t *testing.T, indexer Indexer) {
|
|||||||
// Test 1: All field matches username
|
// Test 1: All field matches username
|
||||||
t.Run("AllFieldMatchesUsername", func(t *testing.T) {
|
t.Run("AllFieldMatchesUsername", func(t *testing.T) {
|
||||||
metadata := SearchGistMetadata{All: "testuser_unique"}
|
metadata := SearchGistMetadata{All: "testuser_unique"}
|
||||||
gistIDs, total, _, err := indexer.Search("", metadata, 100, 1)
|
gistIDs, total, _, err := indexer.Search(metadata, 100, 1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("All field search failed: %v", err)
|
t.Fatalf("All field search failed: %v", err)
|
||||||
}
|
}
|
||||||
@@ -777,7 +777,7 @@ func testIndexerAllFieldSearch(t *testing.T, indexer Indexer) {
|
|||||||
// Test 2: All field matches title
|
// Test 2: All field matches title
|
||||||
t.Run("AllFieldMatchesTitle", func(t *testing.T) {
|
t.Run("AllFieldMatchesTitle", func(t *testing.T) {
|
||||||
metadata := SearchGistMetadata{All: "unique features"}
|
metadata := SearchGistMetadata{All: "unique features"}
|
||||||
gistIDs, total, _, err := indexer.Search("", metadata, 100, 1)
|
gistIDs, total, _, err := indexer.Search(metadata, 100, 1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("All field search failed: %v", err)
|
t.Fatalf("All field search failed: %v", err)
|
||||||
}
|
}
|
||||||
@@ -799,7 +799,7 @@ func testIndexerAllFieldSearch(t *testing.T, indexer Indexer) {
|
|||||||
// Test 3: All field matches language
|
// Test 3: All field matches language
|
||||||
t.Run("AllFieldMatchesLanguage", func(t *testing.T) {
|
t.Run("AllFieldMatchesLanguage", func(t *testing.T) {
|
||||||
metadata := SearchGistMetadata{All: "Ruby"}
|
metadata := SearchGistMetadata{All: "Ruby"}
|
||||||
gistIDs, total, _, err := indexer.Search("", metadata, 100, 1)
|
gistIDs, total, _, err := indexer.Search(metadata, 100, 1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("All field search failed: %v", err)
|
t.Fatalf("All field search failed: %v", err)
|
||||||
}
|
}
|
||||||
@@ -821,7 +821,7 @@ func testIndexerAllFieldSearch(t *testing.T, indexer Indexer) {
|
|||||||
// Test 4: All field matches topic
|
// Test 4: All field matches topic
|
||||||
t.Run("AllFieldMatchesTopic", func(t *testing.T) {
|
t.Run("AllFieldMatchesTopic", func(t *testing.T) {
|
||||||
metadata := SearchGistMetadata{All: "unique_topic"}
|
metadata := SearchGistMetadata{All: "unique_topic"}
|
||||||
gistIDs, total, _, err := indexer.Search("", metadata, 100, 1)
|
gistIDs, total, _, err := indexer.Search(metadata, 100, 1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("All field search failed: %v", err)
|
t.Fatalf("All field search failed: %v", err)
|
||||||
}
|
}
|
||||||
@@ -843,7 +843,7 @@ func testIndexerAllFieldSearch(t *testing.T, indexer Indexer) {
|
|||||||
// Test 5: All field matches extension
|
// Test 5: All field matches extension
|
||||||
t.Run("AllFieldMatchesExtension", func(t *testing.T) {
|
t.Run("AllFieldMatchesExtension", func(t *testing.T) {
|
||||||
metadata := SearchGistMetadata{All: "sh"}
|
metadata := SearchGistMetadata{All: "sh"}
|
||||||
gistIDs, total, _, err := indexer.Search("", metadata, 100, 1)
|
gistIDs, total, _, err := indexer.Search(metadata, 100, 1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("All field search failed: %v", err)
|
t.Fatalf("All field search failed: %v", err)
|
||||||
}
|
}
|
||||||
@@ -865,7 +865,7 @@ func testIndexerAllFieldSearch(t *testing.T, indexer Indexer) {
|
|||||||
// Test 6: All field matches filename
|
// Test 6: All field matches filename
|
||||||
t.Run("AllFieldMatchesFilename", func(t *testing.T) {
|
t.Run("AllFieldMatchesFilename", func(t *testing.T) {
|
||||||
metadata := SearchGistMetadata{All: "unique_file.rb"}
|
metadata := SearchGistMetadata{All: "unique_file.rb"}
|
||||||
gistIDs, total, _, err := indexer.Search("", metadata, 100, 1)
|
gistIDs, total, _, err := indexer.Search(metadata, 100, 1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("All field search failed: %v", err)
|
t.Fatalf("All field search failed: %v", err)
|
||||||
}
|
}
|
||||||
@@ -888,7 +888,7 @@ func testIndexerAllFieldSearch(t *testing.T, indexer Indexer) {
|
|||||||
t.Run("AllFieldORBehavior", func(t *testing.T) {
|
t.Run("AllFieldORBehavior", func(t *testing.T) {
|
||||||
// "unique" appears in: username (3001), title (3002), topic (3003), filename (3004)
|
// "unique" appears in: username (3001), title (3002), topic (3003), filename (3004)
|
||||||
metadata := SearchGistMetadata{All: "unique"}
|
metadata := SearchGistMetadata{All: "unique"}
|
||||||
gistIDs, total, _, err := indexer.Search("", metadata, 100, 1)
|
gistIDs, total, _, err := indexer.Search(metadata, 100, 1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("All field OR search failed: %v", err)
|
t.Fatalf("All field OR search failed: %v", err)
|
||||||
}
|
}
|
||||||
@@ -916,14 +916,14 @@ func testIndexerAllFieldSearch(t *testing.T, indexer Indexer) {
|
|||||||
t.Run("AllFieldVsSpecificField", func(t *testing.T) {
|
t.Run("AllFieldVsSpecificField", func(t *testing.T) {
|
||||||
// Search with All field
|
// Search with All field
|
||||||
metadataAll := SearchGistMetadata{All: "unique"}
|
metadataAll := SearchGistMetadata{All: "unique"}
|
||||||
_, totalAll, _, err := indexer.Search("", metadataAll, 100, 1)
|
_, totalAll, _, err := indexer.Search(metadataAll, 100, 1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("All field search failed: %v", err)
|
t.Fatalf("All field search failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Search with specific username field only
|
// Search with specific username field only
|
||||||
metadataSpecific := SearchGistMetadata{Username: "testuser_unique"}
|
metadataSpecific := SearchGistMetadata{Username: "testuser_unique"}
|
||||||
_, totalSpecific, _, err := indexer.Search("", metadataSpecific, 100, 1)
|
_, totalSpecific, _, err := indexer.Search(metadataSpecific, 100, 1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Specific field search failed: %v", err)
|
t.Fatalf("Specific field search failed: %v", err)
|
||||||
}
|
}
|
||||||
@@ -936,8 +936,8 @@ func testIndexerAllFieldSearch(t *testing.T, indexer Indexer) {
|
|||||||
|
|
||||||
// Test 9: All field with no matches
|
// Test 9: All field with no matches
|
||||||
t.Run("AllFieldNoMatches", func(t *testing.T) {
|
t.Run("AllFieldNoMatches", func(t *testing.T) {
|
||||||
metadata := SearchGistMetadata{All: "nonexistentvalue12345"}
|
metadata := SearchGistMetadata{All: "nonexistentvalue"}
|
||||||
gistIDs, total, _, err := indexer.Search("", metadata, 100, 1)
|
gistIDs, total, _, err := indexer.Search(metadata, 100, 1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("All field no match search failed: %v", err)
|
t.Fatalf("All field no match search failed: %v", err)
|
||||||
}
|
}
|
||||||
@@ -957,7 +957,7 @@ func testIndexerAllFieldSearch(t *testing.T, indexer Indexer) {
|
|||||||
Username: "nonexistent", // This should be ignored
|
Username: "nonexistent", // This should be ignored
|
||||||
Language: "NonExistent", // This should be ignored
|
Language: "NonExistent", // This should be ignored
|
||||||
}
|
}
|
||||||
gistIDs, total, _, err := indexer.Search("", metadata, 100, 1)
|
gistIDs, total, _, err := indexer.Search(metadata, 100, 1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("All field with other fields search failed: %v", err)
|
t.Fatalf("All field with other fields search failed: %v", err)
|
||||||
}
|
}
|
||||||
@@ -983,7 +983,9 @@ func testIndexerAllFieldSearch(t *testing.T, indexer Indexer) {
|
|||||||
// All field searches metadata, content query searches content
|
// All field searches metadata, content query searches content
|
||||||
// Both should work together
|
// Both should work together
|
||||||
metadata := SearchGistMetadata{All: "Ruby"}
|
metadata := SearchGistMetadata{All: "Ruby"}
|
||||||
gistIDs, total, _, err := indexer.Search("examples", metadata, 100, 1)
|
// Use All field for content search
|
||||||
|
metadata.All = "examples"
|
||||||
|
gistIDs, total, _, err := indexer.Search(metadata, 100, 1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("All field with content query failed: %v", err)
|
t.Fatalf("All field with content query failed: %v", err)
|
||||||
}
|
}
|
||||||
@@ -1007,7 +1009,7 @@ func testIndexerAllFieldSearch(t *testing.T, indexer Indexer) {
|
|||||||
t.Run("AllFieldCaseInsensitive", func(t *testing.T) {
|
t.Run("AllFieldCaseInsensitive", func(t *testing.T) {
|
||||||
// Search with different case
|
// Search with different case
|
||||||
metadata := SearchGistMetadata{All: "RUBY"}
|
metadata := SearchGistMetadata{All: "RUBY"}
|
||||||
gistIDs, total, _, err := indexer.Search("", metadata, 100, 1)
|
gistIDs, total, _, err := indexer.Search(metadata, 100, 1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("All field case insensitive search failed: %v", err)
|
t.Fatalf("All field case insensitive search failed: %v", err)
|
||||||
}
|
}
|
||||||
@@ -1101,7 +1103,7 @@ func testIndexerFuzzySearch(t *testing.T, indexer Indexer) {
|
|||||||
|
|
||||||
// Test 1: Exact match should work
|
// Test 1: Exact match should work
|
||||||
t.Run("ExactMatch", func(t *testing.T) {
|
t.Run("ExactMatch", func(t *testing.T) {
|
||||||
gistIDs, total, _, err := indexer.Search("algorithms", SearchGistMetadata{}, 100, 1)
|
gistIDs, total, _, err := indexer.Search(SearchGistMetadata{All: "algorithms"}, 100, 1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Exact match search failed: %v", err)
|
t.Fatalf("Exact match search failed: %v", err)
|
||||||
}
|
}
|
||||||
@@ -1123,7 +1125,7 @@ func testIndexerFuzzySearch(t *testing.T, indexer Indexer) {
|
|||||||
// Test 2: 1 character typo - substitution
|
// Test 2: 1 character typo - substitution
|
||||||
t.Run("OneCharSubstitution", func(t *testing.T) {
|
t.Run("OneCharSubstitution", func(t *testing.T) {
|
||||||
// "algoritm" instead of "algorithm" (missing 'h')
|
// "algoritm" instead of "algorithm" (missing 'h')
|
||||||
gistIDs, total, _, err := indexer.Search("algoritm", SearchGistMetadata{}, 100, 1)
|
gistIDs, total, _, err := indexer.Search(SearchGistMetadata{All: "algoritm"}, 100, 1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("1-char typo search failed: %v", err)
|
t.Fatalf("1-char typo search failed: %v", err)
|
||||||
}
|
}
|
||||||
@@ -1145,7 +1147,7 @@ func testIndexerFuzzySearch(t *testing.T, indexer Indexer) {
|
|||||||
// Test 3: 1 character typo - deletion
|
// Test 3: 1 character typo - deletion
|
||||||
t.Run("OneCharDeletion", func(t *testing.T) {
|
t.Run("OneCharDeletion", func(t *testing.T) {
|
||||||
// "pythn" instead of "python" (missing 'o')
|
// "pythn" instead of "python" (missing 'o')
|
||||||
gistIDs, total, _, err := indexer.Search("pythn", SearchGistMetadata{}, 100, 1)
|
gistIDs, total, _, err := indexer.Search(SearchGistMetadata{All: "pythn"}, 100, 1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("1-char deletion search failed: %v", err)
|
t.Fatalf("1-char deletion search failed: %v", err)
|
||||||
}
|
}
|
||||||
@@ -1167,7 +1169,7 @@ func testIndexerFuzzySearch(t *testing.T, indexer Indexer) {
|
|||||||
// Test 4: 1 character typo - insertion (extra character)
|
// Test 4: 1 character typo - insertion (extra character)
|
||||||
t.Run("OneCharInsertion", func(t *testing.T) {
|
t.Run("OneCharInsertion", func(t *testing.T) {
|
||||||
// "pythonn" instead of "python" (extra 'n')
|
// "pythonn" instead of "python" (extra 'n')
|
||||||
gistIDs, total, _, err := indexer.Search("pythonn", SearchGistMetadata{}, 100, 1)
|
gistIDs, total, _, err := indexer.Search(SearchGistMetadata{All: "pythonn"}, 100, 1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("1-char insertion search failed: %v", err)
|
t.Fatalf("1-char insertion search failed: %v", err)
|
||||||
}
|
}
|
||||||
@@ -1189,7 +1191,7 @@ func testIndexerFuzzySearch(t *testing.T, indexer Indexer) {
|
|||||||
// Test 5: 2 character typos - should still match with fuzziness=2
|
// Test 5: 2 character typos - should still match with fuzziness=2
|
||||||
t.Run("TwoCharTypos", func(t *testing.T) {
|
t.Run("TwoCharTypos", func(t *testing.T) {
|
||||||
// "databse" instead of "database" (missing 'a', transposed 's')
|
// "databse" instead of "database" (missing 'a', transposed 's')
|
||||||
gistIDs, total, _, err := indexer.Search("databse", SearchGistMetadata{}, 100, 1)
|
gistIDs, total, _, err := indexer.Search(SearchGistMetadata{All: "databse"}, 100, 1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("2-char typo search failed: %v", err)
|
t.Fatalf("2-char typo search failed: %v", err)
|
||||||
}
|
}
|
||||||
@@ -1211,7 +1213,7 @@ func testIndexerFuzzySearch(t *testing.T, indexer Indexer) {
|
|||||||
// Test 6: 2 character typos - different word
|
// Test 6: 2 character typos - different word
|
||||||
t.Run("TwoCharTyposDifferentWord", func(t *testing.T) {
|
t.Run("TwoCharTyposDifferentWord", func(t *testing.T) {
|
||||||
// "javasript" instead of "javascript" (missing 'c')
|
// "javasript" instead of "javascript" (missing 'c')
|
||||||
gistIDs, total, _, err := indexer.Search("javasript", SearchGistMetadata{}, 100, 1)
|
gistIDs, total, _, err := indexer.Search(SearchGistMetadata{All: "javasript"}, 100, 1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("2-char typo search failed: %v", err)
|
t.Fatalf("2-char typo search failed: %v", err)
|
||||||
}
|
}
|
||||||
@@ -1233,7 +1235,7 @@ func testIndexerFuzzySearch(t *testing.T, indexer Indexer) {
|
|||||||
// Test 7: 3 character typos - should NOT match (beyond fuzziness=2)
|
// Test 7: 3 character typos - should NOT match (beyond fuzziness=2)
|
||||||
t.Run("ThreeCharTyposShouldNotMatch", func(t *testing.T) {
|
t.Run("ThreeCharTyposShouldNotMatch", func(t *testing.T) {
|
||||||
// "algorthm" instead of "algorithm" (missing 'i', 't', 'h') - too different
|
// "algorthm" instead of "algorithm" (missing 'i', 't', 'h') - too different
|
||||||
gistIDs, _, _, err := indexer.Search("algorthm", SearchGistMetadata{}, 100, 1)
|
gistIDs, _, _, err := indexer.Search(SearchGistMetadata{All: "algorthm"}, 100, 1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("3-char typo search failed: %v", err)
|
t.Fatalf("3-char typo search failed: %v", err)
|
||||||
}
|
}
|
||||||
@@ -1256,7 +1258,7 @@ func testIndexerFuzzySearch(t *testing.T, indexer Indexer) {
|
|||||||
// Test 8: Transposition (swapped characters)
|
// Test 8: Transposition (swapped characters)
|
||||||
t.Run("CharacterTransposition", func(t *testing.T) {
|
t.Run("CharacterTransposition", func(t *testing.T) {
|
||||||
// "pyhton" instead of "python" (swapped 'ht')
|
// "pyhton" instead of "python" (swapped 'ht')
|
||||||
gistIDs, total, _, err := indexer.Search("pyhton", SearchGistMetadata{}, 100, 1)
|
gistIDs, total, _, err := indexer.Search(SearchGistMetadata{All: "pyhton"}, 100, 1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Transposition search failed: %v", err)
|
t.Fatalf("Transposition search failed: %v", err)
|
||||||
}
|
}
|
||||||
@@ -1278,7 +1280,7 @@ func testIndexerFuzzySearch(t *testing.T, indexer Indexer) {
|
|||||||
// Test 9: Case insensitivity with fuzzy search
|
// Test 9: Case insensitivity with fuzzy search
|
||||||
t.Run("CaseInsensitiveWithFuzzy", func(t *testing.T) {
|
t.Run("CaseInsensitiveWithFuzzy", func(t *testing.T) {
|
||||||
// "PYTHN" (uppercase with typo)
|
// "PYTHN" (uppercase with typo)
|
||||||
gistIDs, total, _, err := indexer.Search("PYTHN", SearchGistMetadata{}, 100, 1)
|
gistIDs, total, _, err := indexer.Search(SearchGistMetadata{All: "PYTHN"}, 100, 1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Case insensitive fuzzy search failed: %v", err)
|
t.Fatalf("Case insensitive fuzzy search failed: %v", err)
|
||||||
}
|
}
|
||||||
@@ -1300,7 +1302,7 @@ func testIndexerFuzzySearch(t *testing.T, indexer Indexer) {
|
|||||||
// Test 10: Multiple words with typos
|
// Test 10: Multiple words with typos
|
||||||
t.Run("MultipleWordsWithTypos", func(t *testing.T) {
|
t.Run("MultipleWordsWithTypos", func(t *testing.T) {
|
||||||
// "relatonal databse" instead of "relational database"
|
// "relatonal databse" instead of "relational database"
|
||||||
gistIDs, total, _, err := indexer.Search("relatonal databse", SearchGistMetadata{}, 100, 1)
|
gistIDs, total, _, err := indexer.Search(SearchGistMetadata{All: "relatonal databse"}, 100, 1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Multi-word fuzzy search failed: %v", err)
|
t.Fatalf("Multi-word fuzzy search failed: %v", err)
|
||||||
}
|
}
|
||||||
@@ -1322,7 +1324,7 @@ func testIndexerFuzzySearch(t *testing.T, indexer Indexer) {
|
|||||||
// Test 11: Short words with typos (edge case)
|
// Test 11: Short words with typos (edge case)
|
||||||
t.Run("ShortWordsWithTypos", func(t *testing.T) {
|
t.Run("ShortWordsWithTypos", func(t *testing.T) {
|
||||||
// "SLQ" instead of "SQL" (1 char typo on short word)
|
// "SLQ" instead of "SQL" (1 char typo on short word)
|
||||||
gistIDs, total, _, err := indexer.Search("SLQ", SearchGistMetadata{}, 100, 1)
|
gistIDs, total, _, err := indexer.Search(SearchGistMetadata{All: "SLQ"}, 100, 1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Short word fuzzy search failed: %v", err)
|
t.Fatalf("Short word fuzzy search failed: %v", err)
|
||||||
}
|
}
|
||||||
@@ -1345,7 +1347,8 @@ func testIndexerFuzzySearch(t *testing.T, indexer Indexer) {
|
|||||||
t.Run("FuzzySearchWithMetadataFilters", func(t *testing.T) {
|
t.Run("FuzzySearchWithMetadataFilters", func(t *testing.T) {
|
||||||
// Search with typo AND username filter
|
// Search with typo AND username filter
|
||||||
metadata := SearchGistMetadata{Username: "fuzzytest"}
|
metadata := SearchGistMetadata{Username: "fuzzytest"}
|
||||||
gistIDs, total, _, err := indexer.Search("algoritm", metadata, 100, 1)
|
metadata.All = "algoritm"
|
||||||
|
gistIDs, total, _, err := indexer.Search(metadata, 100, 1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Fuzzy search with metadata failed: %v", err)
|
t.Fatalf("Fuzzy search with metadata failed: %v", err)
|
||||||
}
|
}
|
||||||
@@ -1371,7 +1374,7 @@ func testIndexerPagination(t *testing.T, indexer Indexer) {
|
|||||||
// Test 1: Basic pagination - pages should be different
|
// Test 1: Basic pagination - pages should be different
|
||||||
t.Run("BasicPagination", func(t *testing.T) {
|
t.Run("BasicPagination", func(t *testing.T) {
|
||||||
// Search as user 1 (alice) - should see 334 public gists
|
// Search as user 1 (alice) - should see 334 public gists
|
||||||
gistIDs1, total, _, err := indexer.Search("", SearchGistMetadata{}, 1, 1)
|
gistIDs1, total, _, err := indexer.Search(SearchGistMetadata{}, 1, 1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Page 1 search failed: %v", err)
|
t.Fatalf("Page 1 search failed: %v", err)
|
||||||
}
|
}
|
||||||
@@ -1382,7 +1385,7 @@ func testIndexerPagination(t *testing.T, indexer Indexer) {
|
|||||||
t.Fatal("Expected results on page 1")
|
t.Fatal("Expected results on page 1")
|
||||||
}
|
}
|
||||||
|
|
||||||
gistIDs2, _, _, err := indexer.Search("", SearchGistMetadata{}, 1, 2)
|
gistIDs2, _, _, err := indexer.Search(SearchGistMetadata{}, 1, 2)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Page 2 search failed: %v", err)
|
t.Fatalf("Page 2 search failed: %v", err)
|
||||||
}
|
}
|
||||||
@@ -1398,7 +1401,7 @@ func testIndexerPagination(t *testing.T, indexer Indexer) {
|
|||||||
|
|
||||||
// Test 2: Page size - verify results per page (page size = 10)
|
// Test 2: Page size - verify results per page (page size = 10)
|
||||||
t.Run("PageSizeVerification", func(t *testing.T) {
|
t.Run("PageSizeVerification", func(t *testing.T) {
|
||||||
gistIDs1, _, _, err := indexer.Search("", SearchGistMetadata{}, 1, 1)
|
gistIDs1, _, _, err := indexer.Search(SearchGistMetadata{}, 1, 1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Page 1 search failed: %v", err)
|
t.Fatalf("Page 1 search failed: %v", err)
|
||||||
}
|
}
|
||||||
@@ -1410,11 +1413,11 @@ func testIndexerPagination(t *testing.T, indexer Indexer) {
|
|||||||
|
|
||||||
// Test 3: Total count consistency across pages
|
// Test 3: Total count consistency across pages
|
||||||
t.Run("TotalCountConsistency", func(t *testing.T) {
|
t.Run("TotalCountConsistency", func(t *testing.T) {
|
||||||
_, total1, _, err := indexer.Search("", SearchGistMetadata{}, 1, 1)
|
_, total1, _, err := indexer.Search(SearchGistMetadata{}, 1, 1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Page 1 search failed: %v", err)
|
t.Fatalf("Page 1 search failed: %v", err)
|
||||||
}
|
}
|
||||||
_, total2, _, err := indexer.Search("", SearchGistMetadata{}, 1, 2)
|
_, total2, _, err := indexer.Search(SearchGistMetadata{}, 1, 2)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Page 2 search failed: %v", err)
|
t.Fatalf("Page 2 search failed: %v", err)
|
||||||
}
|
}
|
||||||
@@ -1429,7 +1432,7 @@ func testIndexerPagination(t *testing.T, indexer Indexer) {
|
|||||||
// Test 4: Out of bounds page
|
// Test 4: Out of bounds page
|
||||||
t.Run("OutOfBoundsPage", func(t *testing.T) {
|
t.Run("OutOfBoundsPage", func(t *testing.T) {
|
||||||
// Page 100 is way beyond 334 results with page size 10
|
// Page 100 is way beyond 334 results with page size 10
|
||||||
gistIDs, _, _, err := indexer.Search("", SearchGistMetadata{}, 1, 100)
|
gistIDs, _, _, err := indexer.Search(SearchGistMetadata{}, 1, 100)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Out of bounds page search failed: %v", err)
|
t.Fatalf("Out of bounds page search failed: %v", err)
|
||||||
}
|
}
|
||||||
@@ -1440,7 +1443,8 @@ func testIndexerPagination(t *testing.T, indexer Indexer) {
|
|||||||
|
|
||||||
// Test 5: Empty results pagination
|
// Test 5: Empty results pagination
|
||||||
t.Run("EmptyResultsPagination", func(t *testing.T) {
|
t.Run("EmptyResultsPagination", func(t *testing.T) {
|
||||||
gistIDs, total, _, err := indexer.Search("nonexistentquery", SearchGistMetadata{}, 1, 1)
|
metadata := SearchGistMetadata{All: "nonexistentquery"}
|
||||||
|
gistIDs, total, _, err := indexer.Search(metadata, 1, 1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Empty search failed: %v", err)
|
t.Fatalf("Empty search failed: %v", err)
|
||||||
}
|
}
|
||||||
@@ -1454,11 +1458,11 @@ func testIndexerPagination(t *testing.T, indexer Indexer) {
|
|||||||
|
|
||||||
// Test 6: No duplicate IDs across pages (accounting for +1 overlap for hasMore indicator)
|
// Test 6: No duplicate IDs across pages (accounting for +1 overlap for hasMore indicator)
|
||||||
t.Run("NoDuplicateIDs", func(t *testing.T) {
|
t.Run("NoDuplicateIDs", func(t *testing.T) {
|
||||||
gistIDs1, _, _, err := indexer.Search("", SearchGistMetadata{}, 1, 1)
|
gistIDs1, _, _, err := indexer.Search(SearchGistMetadata{}, 1, 1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Page 1 search failed: %v", err)
|
t.Fatalf("Page 1 search failed: %v", err)
|
||||||
}
|
}
|
||||||
gistIDs2, _, _, err := indexer.Search("", SearchGistMetadata{}, 1, 2)
|
gistIDs2, _, _, err := indexer.Search(SearchGistMetadata{}, 1, 2)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Page 2 search failed: %v", err)
|
t.Fatalf("Page 2 search failed: %v", err)
|
||||||
}
|
}
|
||||||
@@ -1489,7 +1493,7 @@ func testIndexerPagination(t *testing.T, indexer Indexer) {
|
|||||||
t.Run("PaginationWithFilters", func(t *testing.T) {
|
t.Run("PaginationWithFilters", func(t *testing.T) {
|
||||||
// Filter by alice's username - should get 334 gists
|
// Filter by alice's username - should get 334 gists
|
||||||
metadata := SearchGistMetadata{Username: "alice"}
|
metadata := SearchGistMetadata{Username: "alice"}
|
||||||
gistIDs1, total, _, err := indexer.Search("", metadata, 1, 1)
|
gistIDs1, total, _, err := indexer.Search(metadata, 1, 1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Filtered page 1 search failed: %v", err)
|
t.Fatalf("Filtered page 1 search failed: %v", err)
|
||||||
}
|
}
|
||||||
@@ -1500,7 +1504,7 @@ func testIndexerPagination(t *testing.T, indexer Indexer) {
|
|||||||
t.Error("Expected results on filtered page 1")
|
t.Error("Expected results on filtered page 1")
|
||||||
}
|
}
|
||||||
|
|
||||||
gistIDs2, _, _, err := indexer.Search("", metadata, 1, 2)
|
gistIDs2, _, _, err := indexer.Search(metadata, 1, 2)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Filtered page 2 search failed: %v", err)
|
t.Fatalf("Filtered page 2 search failed: %v", err)
|
||||||
}
|
}
|
||||||
@@ -1518,7 +1522,7 @@ func testIndexerPagination(t *testing.T, indexer Indexer) {
|
|||||||
t.Run("LastPageVerification", func(t *testing.T) {
|
t.Run("LastPageVerification", func(t *testing.T) {
|
||||||
// With 334 results and page size 10, page 34 should have 4 results
|
// With 334 results and page size 10, page 34 should have 4 results
|
||||||
// Let's just verify the last page has some results
|
// Let's just verify the last page has some results
|
||||||
gistIDs34, total, _, err := indexer.Search("", SearchGistMetadata{}, 1, 34)
|
gistIDs34, total, _, err := indexer.Search(SearchGistMetadata{}, 1, 34)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Last page search failed: %v", err)
|
t.Fatalf("Last page search failed: %v", err)
|
||||||
}
|
}
|
||||||
@@ -1530,7 +1534,7 @@ func testIndexerPagination(t *testing.T, indexer Indexer) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Page 35 should be empty
|
// Page 35 should be empty
|
||||||
gistIDs35, _, _, err := indexer.Search("", SearchGistMetadata{}, 1, 35)
|
gistIDs35, _, _, err := indexer.Search(SearchGistMetadata{}, 1, 35)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Beyond last page search failed: %v", err)
|
t.Fatalf("Beyond last page search failed: %v", err)
|
||||||
}
|
}
|
||||||
@@ -1541,15 +1545,15 @@ func testIndexerPagination(t *testing.T, indexer Indexer) {
|
|||||||
|
|
||||||
// Test 9: Multiple pages have different results
|
// Test 9: Multiple pages have different results
|
||||||
t.Run("MultiplePagesDifferent", func(t *testing.T) {
|
t.Run("MultiplePagesDifferent", func(t *testing.T) {
|
||||||
gistIDs1, _, _, err := indexer.Search("", SearchGistMetadata{}, 1, 1)
|
gistIDs1, _, _, err := indexer.Search(SearchGistMetadata{}, 1, 1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Page 1 search failed: %v", err)
|
t.Fatalf("Page 1 search failed: %v", err)
|
||||||
}
|
}
|
||||||
gistIDs10, _, _, err := indexer.Search("", SearchGistMetadata{}, 1, 10)
|
gistIDs10, _, _, err := indexer.Search(SearchGistMetadata{}, 1, 10)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Page 10 search failed: %v", err)
|
t.Fatalf("Page 10 search failed: %v", err)
|
||||||
}
|
}
|
||||||
gistIDs20, _, _, err := indexer.Search("", SearchGistMetadata{}, 1, 20)
|
gistIDs20, _, _, err := indexer.Search(SearchGistMetadata{}, 1, 20)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Page 20 search failed: %v", err)
|
t.Fatalf("Page 20 search failed: %v", err)
|
||||||
}
|
}
|
||||||
@@ -1568,7 +1572,7 @@ func testIndexerPagination(t *testing.T, indexer Indexer) {
|
|||||||
// Test 10: Pagination with different users (visibility filtering)
|
// Test 10: Pagination with different users (visibility filtering)
|
||||||
t.Run("PaginationWithVisibility", func(t *testing.T) {
|
t.Run("PaginationWithVisibility", func(t *testing.T) {
|
||||||
// User 2 (bob) sees 667 gists (334 public alice + 333 own private)
|
// User 2 (bob) sees 667 gists (334 public alice + 333 own private)
|
||||||
gistIDs1Bob, totalBob, _, err := indexer.Search("", SearchGistMetadata{}, 2, 1)
|
gistIDs1Bob, totalBob, _, err := indexer.Search(SearchGistMetadata{}, 2, 1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Bob page 1 search failed: %v", err)
|
t.Fatalf("Bob page 1 search failed: %v", err)
|
||||||
}
|
}
|
||||||
@@ -1580,7 +1584,7 @@ func testIndexerPagination(t *testing.T, indexer Indexer) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// User 1 (alice) sees 334 gists
|
// User 1 (alice) sees 334 gists
|
||||||
_, totalAlice, _, err := indexer.Search("", SearchGistMetadata{}, 1, 1)
|
_, totalAlice, _, err := indexer.Search(SearchGistMetadata{}, 1, 1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Alice page 1 search failed: %v", err)
|
t.Fatalf("Alice page 1 search failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
|
|
||||||
"github.com/meilisearch/meilisearch-go"
|
"github.com/meilisearch/meilisearch-go"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
|
"github.com/thomiceli/opengist/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
type MeiliIndexer struct {
|
type MeiliIndexer struct {
|
||||||
@@ -63,9 +64,9 @@ func (i *MeiliIndexer) open() (meilisearch.IndexManager, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_, _ = i.client.Index(i.indexName).UpdateSettings(&meilisearch.Settings{
|
_, _ = i.client.Index(i.indexName).UpdateSettings(&meilisearch.Settings{
|
||||||
FilterableAttributes: []string{"GistID", "UserID", "Visibility", "Username", "Title", "Filenames", "Extensions", "Languages", "Topics"},
|
FilterableAttributes: []string{"GistID", "UserID", "Visibility", "Username", "Title", "Description", "Filenames", "Extensions", "Languages", "Topics"},
|
||||||
DisplayedAttributes: []string{"GistID"},
|
DisplayedAttributes: []string{"GistID"},
|
||||||
SearchableAttributes: []string{"Content", "Username", "Title", "Filenames", "Extensions", "Languages", "Topics"},
|
SearchableAttributes: []string{"Content", "Username", "Title", "Description", "Filenames", "Extensions", "Languages", "Topics"},
|
||||||
RankingRules: []string{"words"},
|
RankingRules: []string{"words"},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -109,7 +110,7 @@ func (i *MeiliIndexer) Remove(gistID uint) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *MeiliIndexer) Search(queryStr string, queryMetadata SearchGistMetadata, userId uint, page int) ([]uint, uint64, map[string]int, error) {
|
func (i *MeiliIndexer) Search(queryMetadata SearchGistMetadata, userId uint, page int) ([]uint, uint64, map[string]int, error) {
|
||||||
searchRequest := &meilisearch.SearchRequest{
|
searchRequest := &meilisearch.SearchRequest{
|
||||||
Offset: int64((page - 1) * 10),
|
Offset: int64((page - 1) * 10),
|
||||||
Limit: 11,
|
Limit: 11,
|
||||||
@@ -128,6 +129,7 @@ func (i *MeiliIndexer) Search(queryStr string, queryMetadata SearchGistMetadata,
|
|||||||
}
|
}
|
||||||
addFilter("Username", queryMetadata.Username)
|
addFilter("Username", queryMetadata.Username)
|
||||||
addFilter("Title", queryMetadata.Title)
|
addFilter("Title", queryMetadata.Title)
|
||||||
|
addFilter("Description", queryMetadata.Description)
|
||||||
addFilter("Filenames", queryMetadata.Filename)
|
addFilter("Filenames", queryMetadata.Filename)
|
||||||
addFilter("Extensions", queryMetadata.Extension)
|
addFilter("Extensions", queryMetadata.Extension)
|
||||||
addFilter("Languages", queryMetadata.Language)
|
addFilter("Languages", queryMetadata.Language)
|
||||||
@@ -137,7 +139,29 @@ func (i *MeiliIndexer) Search(queryStr string, queryMetadata SearchGistMetadata,
|
|||||||
searchRequest.Filter = strings.Join(filters, " AND ")
|
searchRequest.Filter = strings.Join(filters, " AND ")
|
||||||
}
|
}
|
||||||
|
|
||||||
response, err := (*atomicIndexer.Load()).(*MeiliIndexer).index.Search(queryStr, searchRequest)
|
// build query string from provided metadata. Prefer `All`, then `Default`, fall back to `Content`.
|
||||||
|
query := queryMetadata.All
|
||||||
|
if query == "" && queryMetadata.Default != "" {
|
||||||
|
query = queryMetadata.Default
|
||||||
|
var fields []string
|
||||||
|
for _, f := range strings.Split(config.C.SearchDefault, ",") {
|
||||||
|
f = strings.TrimSpace(f)
|
||||||
|
if f == "all" {
|
||||||
|
fields = AllSearchFields
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if indexField, ok := SearchFieldMap[f]; ok {
|
||||||
|
fields = append(fields, indexField)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(fields) > 0 {
|
||||||
|
searchRequest.AttributesToSearchOn = fields
|
||||||
|
}
|
||||||
|
} else if query == "" {
|
||||||
|
query = queryMetadata.Content
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := (*atomicIndexer.Load()).(*MeiliIndexer).index.Search(query, searchRequest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("Failed to search Meilisearch index")
|
log.Error().Err(err).Msg("Failed to search Meilisearch index")
|
||||||
return nil, 0, nil, err
|
return nil, 0, nil, err
|
||||||
|
|||||||
@@ -164,6 +164,18 @@ func AllGists(ctx *context.Context) error {
|
|||||||
return ctx.Html("all.html")
|
return ctx.Html("all.html")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Search handles the search page for gists.
|
||||||
|
//
|
||||||
|
// It takes a query parameter "q" which is a search query in the format:
|
||||||
|
// "user:username title:title description:description filename:filename language:language topic:topic"
|
||||||
|
//
|
||||||
|
// It also takes a page parameter "page" which is the page number to display.
|
||||||
|
//
|
||||||
|
// It returns an error if the search query is invalid or if the page number is invalid.
|
||||||
|
//
|
||||||
|
// It returns the search results as a list of rendered gists, along with the total number of results, the languages found, and the search query.
|
||||||
|
//
|
||||||
|
// The search results are paginated, with 10 results per page.
|
||||||
func Search(ctx *context.Context) error {
|
func Search(ctx *context.Context) error {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
@@ -171,7 +183,7 @@ func Search(ctx *context.Context) error {
|
|||||||
Query: ctx.QueryParam("q"),
|
Query: ctx.QueryParam("q"),
|
||||||
}
|
}
|
||||||
|
|
||||||
content, meta := handlers.ParseSearchQueryStr(ctx.QueryParam("q"))
|
metadata := handlers.ParseSearchQueryStr(ctx.QueryParam("q"))
|
||||||
pageInt := handlers.GetPage(ctx)
|
pageInt := handlers.GetPage(ctx)
|
||||||
|
|
||||||
var currentUserId uint
|
var currentUserId uint
|
||||||
@@ -182,14 +194,18 @@ func Search(ctx *context.Context) error {
|
|||||||
currentUserId = 0
|
currentUserId = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
gistsIds, nbHits, langs, err := index.SearchGists(content, index.SearchGistMetadata{
|
// Search gists in the index and fetch the gists IDs from the database
|
||||||
Username: meta["user"],
|
gistsIds, nbHits, langs, err := index.SearchGists(index.SearchGistMetadata{
|
||||||
Title: meta["title"],
|
Username: metadata["user"],
|
||||||
Filename: meta["filename"],
|
Title: metadata["title"],
|
||||||
Extension: meta["extension"],
|
Description: metadata["description"],
|
||||||
Language: meta["language"],
|
Filename: metadata["filename"],
|
||||||
Topic: meta["topic"],
|
Extension: metadata["extension"],
|
||||||
All: meta["all"],
|
Language: metadata["language"],
|
||||||
|
Topic: metadata["topic"],
|
||||||
|
Content: metadata["content"],
|
||||||
|
All: metadata["all"],
|
||||||
|
Default: metadata["default"],
|
||||||
}, currentUserId, pageInt)
|
}, currentUserId, pageInt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ctx.ErrorRes(500, "Error searching gists", err)
|
return ctx.ErrorRes(500, "Error searching gists", err)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/gorilla/schema"
|
"github.com/gorilla/schema"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/thomiceli/opengist/internal/web/context"
|
"github.com/thomiceli/opengist/internal/web/context"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -119,10 +119,16 @@ func Paginate[T any](ctx *context.Context, data []*T, pageInt int, perPage int,
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ParseSearchQueryStr(query string) (string, map[string]string) {
|
// ParseSearchQueryStr parses a search query string and returns a map of metadata.
|
||||||
|
// The query string is split into words and each word is checked if it contains a colon (:).
|
||||||
|
// If a word contains a colon, it is split into a key-value pair and added to the metadata map.
|
||||||
|
// If a word does not contain a colon, it is added to an "all" key in the metadata map.
|
||||||
|
// The "all" key is used to search all fields in the index.
|
||||||
|
// The function returns the metadata map.
|
||||||
|
func ParseSearchQueryStr(query string) map[string]string {
|
||||||
words := strings.Fields(query)
|
words := strings.Fields(query)
|
||||||
metadata := make(map[string]string)
|
metadata := make(map[string]string)
|
||||||
var contentBuilder strings.Builder
|
var allFieldsBuilder strings.Builder
|
||||||
|
|
||||||
for _, word := range words {
|
for _, word := range words {
|
||||||
if strings.Contains(word, ":") {
|
if strings.Contains(word, ":") {
|
||||||
@@ -133,10 +139,18 @@ func ParseSearchQueryStr(query string) (string, map[string]string) {
|
|||||||
metadata[key] = value
|
metadata[key] = value
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
contentBuilder.WriteString(word + " ")
|
// Add to content search by default
|
||||||
|
allFieldsBuilder.WriteString(word + " ")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
content := strings.TrimSpace(contentBuilder.String())
|
// Set the default search field
|
||||||
return content, metadata
|
allContent := strings.TrimSpace(allFieldsBuilder.String())
|
||||||
|
if allContent != "" {
|
||||||
|
metadata["default"] = allContent
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug().Msgf("Metadata: %v", metadata)
|
||||||
|
|
||||||
|
return metadata
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -147,7 +147,10 @@ func (s *Server) setFuncMap() {
|
|||||||
return dict, nil
|
return dict, nil
|
||||||
},
|
},
|
||||||
"addMetadataToSearchQuery": func(input, key, value string) string {
|
"addMetadataToSearchQuery": func(input, key, value string) string {
|
||||||
content, metadata := handlers.ParseSearchQueryStr(input)
|
metadata := handlers.ParseSearchQueryStr(input)
|
||||||
|
// extract free-text content (stored under "all") and remove it from metadata
|
||||||
|
content := metadata["all"]
|
||||||
|
delete(metadata, "all")
|
||||||
|
|
||||||
metadata[key] = value
|
metadata[key] = value
|
||||||
|
|
||||||
|
|||||||
@@ -116,10 +116,12 @@
|
|||||||
<div class="p-4 text-xs space-y-1">
|
<div class="p-4 text-xs space-y-1">
|
||||||
<p class="text-gray-400"><code class="text-slate-800 dark:text-slate-300 pr-1">user:thomas</code> {{ .locale.Tr "gist.search.help.user" }}</p>
|
<p class="text-gray-400"><code class="text-slate-800 dark:text-slate-300 pr-1">user:thomas</code> {{ .locale.Tr "gist.search.help.user" }}</p>
|
||||||
<p class="text-gray-400"><code class="text-slate-800 dark:text-slate-300 pr-1">title:mygist</code> {{ .locale.Tr "gist.search.help.title" }}</p>
|
<p class="text-gray-400"><code class="text-slate-800 dark:text-slate-300 pr-1">title:mygist</code> {{ .locale.Tr "gist.search.help.title" }}</p>
|
||||||
|
<p class="text-gray-400"><code class="text-slate-800 dark:text-slate-300 pr-1">description:sync</code> {{ .locale.Tr "gist.search.help.description" }}</p>
|
||||||
<p class="text-gray-400"><code class="text-slate-800 dark:text-slate-300 pr-1">filename:myfile.txt</code> {{ .locale.Tr "gist.search.help.filename" }}</p>
|
<p class="text-gray-400"><code class="text-slate-800 dark:text-slate-300 pr-1">filename:myfile.txt</code> {{ .locale.Tr "gist.search.help.filename" }}</p>
|
||||||
<p class="text-gray-400"><code class="text-slate-800 dark:text-slate-300 pr-1">extension:yml</code> {{ .locale.Tr "gist.search.help.extension" }}</p>
|
<p class="text-gray-400"><code class="text-slate-800 dark:text-slate-300 pr-1">extension:yml</code> {{ .locale.Tr "gist.search.help.extension" }}</p>
|
||||||
<p class="text-gray-400"><code class="text-slate-800 dark:text-slate-300 pr-1">language:go</code> {{ .locale.Tr "gist.search.help.language" }}</p>
|
<p class="text-gray-400"><code class="text-slate-800 dark:text-slate-300 pr-1">language:go</code> {{ .locale.Tr "gist.search.help.language" }}</p>
|
||||||
<p class="text-gray-400"><code class="text-slate-800 dark:text-slate-300 pr-1">topic:homelab</code> {{ .locale.Tr "gist.search.help.topic" }}</p>
|
<p class="text-gray-400"><code class="text-slate-800 dark:text-slate-300 pr-1">topic:homelab</code> {{ .locale.Tr "gist.search.help.topic" }}</p>
|
||||||
|
<p class="text-gray-400"><code class="text-slate-800 dark:text-slate-300 pr-1">all:systemctl</code> {{ .locale.Tr "gist.search.help.all" }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
المرجع في مشكلة جديدة
حظر مستخدم