// Sync article index on startup func SyncMeilisearch(client *meilisearch.Client, articles []Article) error { index := client.Index("articles") docs := make([]map[string]interface{}, len(articles)) for i, a := range articles { docs[i] = map[string]interface{}{ "id": a.ID, "title": a.Title, "slug": a.Slug, "excerpt": a.Excerpt, "category": a.Category, "tags": a.Tags, "published_at": a.PublishedAt, } } _, err := index.AddDocuments(docs) return err } CODE_BLOCK: // Sync article index on startup func SyncMeilisearch(client *meilisearch.Client, articles []Article) error { index := client.Index("articles") docs := make([]map[string]interface{}, len(articles)) for i, a := range articles { docs[i] = map[string]interface{}{ "id": a.ID, "title": a.Title, "slug": a.Slug, "excerpt": a.Excerpt, "category": a.Category, "tags": a.Tags, "published_at": a.PublishedAt, } } _, err := index.AddDocuments(docs) return err } CODE_BLOCK: // Sync article index on startup func SyncMeilisearch(client *meilisearch.Client, articles []Article) error { index := client.Index("articles") docs := make([]map[string]interface{}, len(articles)) for i, a := range articles { docs[i] = map[string]interface{}{ "id": a.ID, "title": a.Title, "slug": a.Slug, "excerpt": a.Excerpt, "category": a.Category, "tags": a.Tags, "published_at": a.PublishedAt, } } _, err := index.AddDocuments(docs) return err } CODE_BLOCK: GET /api/search?q=kerberoasting&cat=guide&limit=10 CODE_BLOCK: GET /api/search?q=kerberoasting&cat=guide&limit=10 CODE_BLOCK: GET /api/search?q=kerberoasting&cat=guide&limit=10 CODE_BLOCK: results, err := SearchMeilisearch(query, filters) if err != nil || len(results) == 0 { results, err = SearchMySQL(query, filters) // fallback } CODE_BLOCK: results, err := SearchMeilisearch(query, filters) if err != nil || len(results) == 0 { results, err = SearchMySQL(query, filters) // fallback } CODE_BLOCK: results, err := SearchMeilisearch(query, filters) if err != nil || len(results) == 0 { results, err = SearchMySQL(query, filters) // fallback } CODE_BLOCK: User query → Meilisearch (retrieval, ~10-30ms) → Top 3-5 articles (slug + title + excerpt) → LLM prompt context → Generated response / enriched content CODE_BLOCK: User query → Meilisearch (retrieval, ~10-30ms) → Top 3-5 articles (slug + title + excerpt) → LLM prompt context → Generated response / enriched content CODE_BLOCK: User query → Meilisearch (retrieval, ~10-30ms) → Top 3-5 articles (slug + title + excerpt) → LLM prompt context → Generated response / enriched content CODE_BLOCK: { "AD": ["Active Directory"], "pentest": ["penetration test", "intrusion test"], "MFA": ["multi-factor authentication", "2FA"] } CODE_BLOCK: { "AD": ["Active Directory"], "pentest": ["penetration test", "intrusion test"], "MFA": ["multi-factor authentication", "2FA"] } CODE_BLOCK: { "AD": ["Active Directory"], "pentest": ["penetration test", "intrusion test"], "MFA": ["multi-factor authentication", "2FA"] }
- Handled typos (users search "kerberosting" not "kerberoasting")
- Returned results in < 50ms
- Could be self-hosted (no SaaS dependency for a small site)
- Had a decent Go client
- Fast keyword + semantic-ish retrieval (Meilisearch handles this with its ranking rules)
- A way to surface the right article given a user query
- Context injection into LLM prompts when generating summaries or related content
- Meilisearch is genuinely good and genuinely easy
- Content quality beats algorithmic cleverness every time
- For domain-specific retrieval, you don't need vector embeddings unless your queries are conversational/open-ended
- Log your zero-result searches — it's free product research