feat: 新增index/mapping/document/search接口

This commit is contained in:
2025-11-16 23:33:35 +08:00
parent fc14798af5
commit e4e781f6ed
9 changed files with 2137 additions and 11 deletions

View File

@@ -57,7 +57,17 @@ es-demo/
│ └── cluster_test.go # 集群操作测试 │ └── cluster_test.go # 集群操作测试
└── index/ # 索引级别业务操作 └── index/ # 索引级别业务操作
├── template.go # 索引模板管理CRUD ├── template.go # 索引模板管理CRUD
── template_test.go # 模板操作测试 ── template_test.go # 模板操作测试
├── ism.go # ISM 策略管理
├── policy.go # 统一策略接口
├── index.go # 索引 CRUD 操作
├── index_test.go # 索引操作测试
├── mapping.go # 索引 Mapping 管理
├── mapping_test.go # Mapping 操作测试
├── document.go # 文档 CRUD 操作
├── document_test.go # 文档操作测试
├── search.go # 全文搜索功能
└── search_test.go # 搜索功能测试
``` ```
## 开发原则 ## 开发原则
@@ -135,16 +145,49 @@ es-demo/
- ListPolicies: 列出所有策略 - ListPolicies: 列出所有策略
- 策略配置验证 - 策略配置验证
- [x] 索引管理Index Management
- CreateIndex: 创建索引(支持 Settings 和 Mappings 配置)
- GetIndex: 获取索引信息
- DeleteIndex: 删除索引
- IndexExists: 检查索引是否存在
- ListIndices: 列出匹配的索引
- 索引名称验证和错误处理
- [x] Mapping 管理Dynamic Mapping
- PutMapping: 添加/更新字段映射
- GetMapping: 获取索引映射信息
- AddField: 便捷的单字段添加
- FieldMapping 配置Type、Index、Store、Analyzer、Format、IgnoreAbove
- [x] 文档操作Document CRUD
- IndexDocument: 索引文档(支持自动生成或指定 ID
- GetDocument: 根据 ID 获取文档
- UpdateDocument: 部分更新文档
- DeleteDocument: 删除文档
- BulkIndexDocuments: 批量索引文档NDJSON 格式)
- [x] 全文搜索Search
- Search: 主搜索函数(支持分页、排序、字段过滤)
- Query 构建器:
- MatchQuery: 全文匹配查询
- TermQuery: 精确词项查询
- RangeQuery: 范围查询
- BoolQuery: 布尔组合查询Must、Should、MustNot、Filter
- MultiMatchQuery: 多字段匹配
- WildcardQuery: 通配符查询
- PrefixQuery: 前缀查询
- MatchAllQuery: 匹配所有文档
### 待实现功能 ### 待实现功能
以下功能将根据实际需求逐步实现: 以下功能将根据实际需求逐步实现:
- [ ] 索引管理(创建、删除、更新索引 - [ ] 聚合查询Aggregations
- [ ] 文档操作CRUD - [ ] 复杂查询组合(嵌套、父子文档)
- [ ] 搜索功能(基础查询、复杂查询) - [ ] 索引别名管理
- [ ] 聚合查询 - [ ] 快照和恢复
- [ ] 批量操作
- [ ] 性能测试工具 - [ ] 性能测试工具
- [ ] 监控和指标
## 快速开始 ## 快速开始
@@ -225,14 +268,16 @@ go tool cover -html=coverage.out -o coverage.html
- [x] 测试流水线自动化Lint → Build → Test → Cleanup - [x] 测试流水线自动化Lint → Build → Test → Cleanup
- [x] 代码质量保障golangci-lint 集成) - [x] 代码质量保障golangci-lint 集成)
### 第二阶段:核心功能(进行中 ### 第二阶段:核心功能(已完成
- [x] 集群运维操作GetInfo - [x] 集群运维操作GetInfo
- [x] 索引模板管理CRUD - [x] 索引模板管理CRUD
- [ ] 索引管理功能(创建、删除、更新索引 - [x] ISM 策略管理CRUD
- [ ] 文档 CRUD 操作 - [x] 索引管理功能(创建、删除、查询、列表)
- [ ] 基础搜索功能 - [x] Mapping 动态管理(添加字段、获取映射)
- [ ] 完善测试覆盖 - [x] 文档 CRUD 操作(包含批量索引)
- [x] 全文搜索功能(多种查询类型)
- [x] 完善测试覆盖(集成测试 + 单元测试)
### 第三阶段:高级功能 ### 第三阶段:高级功能

View File

@@ -0,0 +1,312 @@
// Package index provides index-level operations for OpenSearch.
package index
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"os"
"es-demo/client"
"github.com/opensearch-project/opensearch-go/v2/opensearchapi"
)
var (
// ErrDocumentNotFound is returned when the specified document does not exist.
ErrDocumentNotFound = fmt.Errorf("document not found")
)
// Document represents a document in OpenSearch.
type Document struct {
ID string
Source map[string]any
}
// IndexDocument indexes a document (create or update).
// If ID is empty, OpenSearch will generate one automatically.
func IndexDocument(ctx context.Context, c *client.Client, indexName string, doc *Document) (string, error) {
if indexName == "" {
return "", ErrInvalidIndexName
}
if doc == nil || doc.Source == nil {
return "", fmt.Errorf("document cannot be nil")
}
data, err := json.Marshal(doc.Source)
if err != nil {
return "", fmt.Errorf("failed to marshal document: %w", err)
}
req := opensearchapi.IndexRequest{
Index: indexName,
DocumentID: doc.ID,
Body: bytes.NewReader(data),
Refresh: "true", // Make document immediately searchable
}
res, err := req.Do(ctx, c.GetClient())
if err != nil {
return "", fmt.Errorf("failed to execute index document request: %w", err)
}
defer func() {
if closeErr := res.Body.Close(); closeErr != nil {
fmt.Fprintf(os.Stderr, "warning: failed to close response body: %v\n", closeErr)
}
}()
if res.IsError() {
bodyBytes, _ := io.ReadAll(res.Body)
return "", fmt.Errorf("index document failed with status %s: %s", res.Status(), string(bodyBytes))
}
// Parse response to get document ID
var response struct {
ID string `json:"_id"`
}
bodyBytes, _ := io.ReadAll(res.Body)
if err := json.Unmarshal(bodyBytes, &response); err != nil {
return "", fmt.Errorf("failed to decode response: %w", err)
}
return response.ID, nil
}
// GetDocument retrieves a document by ID.
func GetDocument(ctx context.Context, c *client.Client, indexName string, docID string) (*Document, error) {
if indexName == "" {
return nil, ErrInvalidIndexName
}
if docID == "" {
return nil, fmt.Errorf("document ID cannot be empty")
}
req := opensearchapi.GetRequest{
Index: indexName,
DocumentID: docID,
}
res, err := req.Do(ctx, c.GetClient())
if err != nil {
return nil, fmt.Errorf("failed to execute get document request: %w", err)
}
defer func() {
if closeErr := res.Body.Close(); closeErr != nil {
fmt.Fprintf(os.Stderr, "warning: failed to close response body: %v\n", closeErr)
}
}()
if res.StatusCode == 404 {
return nil, ErrDocumentNotFound
}
if res.IsError() {
bodyBytes, _ := io.ReadAll(res.Body)
return nil, fmt.Errorf("get document failed with status %s: %s", res.Status(), string(bodyBytes))
}
var response struct {
ID string `json:"_id"`
Found bool `json:"found"`
Source map[string]any `json:"_source"`
}
if err := json.NewDecoder(res.Body).Decode(&response); err != nil {
return nil, fmt.Errorf("failed to decode document: %w", err)
}
if !response.Found {
return nil, ErrDocumentNotFound
}
return &Document{
ID: response.ID,
Source: response.Source,
}, nil
}
// UpdateDocument updates a document by ID using partial update.
func UpdateDocument(ctx context.Context, c *client.Client, indexName string, docID string, partialDoc map[string]any) error {
if indexName == "" {
return ErrInvalidIndexName
}
if docID == "" {
return fmt.Errorf("document ID cannot be empty")
}
if len(partialDoc) == 0 {
return fmt.Errorf("partial document cannot be empty")
}
updateBody := map[string]any{
"doc": partialDoc,
}
data, err := json.Marshal(updateBody)
if err != nil {
return fmt.Errorf("failed to marshal update body: %w", err)
}
req := opensearchapi.UpdateRequest{
Index: indexName,
DocumentID: docID,
Body: bytes.NewReader(data),
Refresh: "true",
}
res, err := req.Do(ctx, c.GetClient())
if err != nil {
return fmt.Errorf("failed to execute update document request: %w", err)
}
defer func() {
if closeErr := res.Body.Close(); closeErr != nil {
fmt.Fprintf(os.Stderr, "warning: failed to close response body: %v\n", closeErr)
}
}()
if res.StatusCode == 404 {
return ErrDocumentNotFound
}
if res.IsError() {
bodyBytes, _ := io.ReadAll(res.Body)
return fmt.Errorf("update document failed with status %s: %s", res.Status(), string(bodyBytes))
}
return nil
}
// DeleteDocument deletes a document by ID.
func DeleteDocument(ctx context.Context, c *client.Client, indexName string, docID string) error {
if indexName == "" {
return ErrInvalidIndexName
}
if docID == "" {
return fmt.Errorf("document ID cannot be empty")
}
req := opensearchapi.DeleteRequest{
Index: indexName,
DocumentID: docID,
Refresh: "true",
}
res, err := req.Do(ctx, c.GetClient())
if err != nil {
return fmt.Errorf("failed to execute delete document request: %w", err)
}
defer func() {
if closeErr := res.Body.Close(); closeErr != nil {
fmt.Fprintf(os.Stderr, "warning: failed to close response body: %v\n", closeErr)
}
}()
if res.StatusCode == 404 {
return ErrDocumentNotFound
}
if res.IsError() {
bodyBytes, _ := io.ReadAll(res.Body)
return fmt.Errorf("delete document failed with status %s: %s", res.Status(), string(bodyBytes))
}
return nil
}
// BulkIndexDocuments indexes multiple documents in a single request.
func BulkIndexDocuments(ctx context.Context, c *client.Client, indexName string, docs []*Document) error {
if indexName == "" {
return ErrInvalidIndexName
}
if len(docs) == 0 {
return fmt.Errorf("documents cannot be empty")
}
// Build bulk request body
var buf bytes.Buffer
for _, doc := range docs {
// Action line
action := map[string]any{
"index": map[string]any{
"_index": indexName,
},
}
if doc.ID != "" {
action["index"].(map[string]any)["_id"] = doc.ID
}
actionData, err := json.Marshal(action)
if err != nil {
return fmt.Errorf("failed to marshal action: %w", err)
}
buf.Write(actionData)
buf.WriteByte('\n')
// Document line
docData, err := json.Marshal(doc.Source)
if err != nil {
return fmt.Errorf("failed to marshal document: %w", err)
}
buf.Write(docData)
buf.WriteByte('\n')
}
req := opensearchapi.BulkRequest{
Body: &buf,
Refresh: "true",
}
res, err := req.Do(ctx, c.GetClient())
if err != nil {
return fmt.Errorf("failed to execute bulk request: %w", err)
}
defer func() {
if closeErr := res.Body.Close(); closeErr != nil {
fmt.Fprintf(os.Stderr, "warning: failed to close response body: %v\n", closeErr)
}
}()
if res.IsError() {
bodyBytes, _ := io.ReadAll(res.Body)
return fmt.Errorf("bulk request failed with status %s: %s", res.Status(), string(bodyBytes))
}
// Check for errors in bulk response
var response struct {
Errors bool `json:"errors"`
Items []map[string]struct {
Error *struct {
Type string `json:"type"`
Reason string `json:"reason"`
} `json:"error"`
} `json:"items"`
}
bodyBytes, _ := io.ReadAll(res.Body)
if err := json.Unmarshal(bodyBytes, &response); err != nil {
return fmt.Errorf("failed to decode bulk response: %w", err)
}
if response.Errors {
// Collect error details
var errorMsg string
for i, item := range response.Items {
for _, itemData := range item {
if itemData.Error != nil {
errorMsg += fmt.Sprintf("doc %d: %s - %s; ", i, itemData.Error.Type, itemData.Error.Reason)
}
}
}
return fmt.Errorf("bulk indexing had errors: %s", errorMsg)
}
return nil
}

View File

@@ -0,0 +1,368 @@
package index
import (
"context"
"testing"
"time"
)
// TestDocumentIntegration tests complete document lifecycle.
func TestDocumentIntegration(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test in short mode")
}
c := setupIntegrationTest(t)
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
indexName := "test-doc-integration"
// Setup index
err := CreateIndex(ctx, c, indexName, nil)
if err != nil {
t.Fatalf("setup: CreateIndex() error = %v", err)
}
defer func() {
_ = DeleteIndex(ctx, c, indexName)
}()
var docID string
// Test 1: Index document
t.Run("IndexDocument", func(t *testing.T) {
doc := &Document{
Source: map[string]any{
"title": "Test Document",
"content": "This is a test",
"count": 42,
},
}
id, err := IndexDocument(ctx, c, indexName, doc)
if err != nil {
t.Fatalf("IndexDocument() error = %v", err)
}
if id == "" {
t.Error("IndexDocument() returned empty ID")
}
docID = id
t.Logf("Document indexed with ID: %s", docID)
})
// Test 2: Get document
t.Run("GetDocument", func(t *testing.T) {
doc, err := GetDocument(ctx, c, indexName, docID)
if err != nil {
t.Fatalf("GetDocument() error = %v", err)
}
if doc.ID != docID {
t.Errorf("GetDocument() ID = %q, want %q", doc.ID, docID)
}
if doc.Source["title"] != "Test Document" {
t.Errorf("GetDocument() title = %v, want %v", doc.Source["title"], "Test Document")
}
t.Logf("Retrieved document: %s", doc.ID)
})
// Test 3: Update document
t.Run("UpdateDocument", func(t *testing.T) {
updates := map[string]any{
"title": "Updated Document",
"count": 100,
}
err := UpdateDocument(ctx, c, indexName, docID, updates)
if err != nil {
t.Fatalf("UpdateDocument() error = %v", err)
}
// Verify update
doc, err := GetDocument(ctx, c, indexName, docID)
if err != nil {
t.Fatalf("GetDocument() error = %v", err)
}
if doc.Source["title"] != "Updated Document" {
t.Errorf("Updated title = %v, want %v", doc.Source["title"], "Updated Document")
}
t.Log("Document updated successfully")
})
// Test 4: Delete document
t.Run("DeleteDocument", func(t *testing.T) {
err := DeleteDocument(ctx, c, indexName, docID)
if err != nil {
t.Fatalf("DeleteDocument() error = %v", err)
}
// Verify deletion
_, err = GetDocument(ctx, c, indexName, docID)
if err != ErrDocumentNotFound {
t.Errorf("GetDocument() error = %v, want %v", err, ErrDocumentNotFound)
}
t.Log("Document deleted successfully")
})
}
func TestBulkIndexDocuments(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test in short mode")
}
c := setupIntegrationTest(t)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
indexName := "test-bulk-index"
// Setup index
err := CreateIndex(ctx, c, indexName, nil)
if err != nil {
t.Fatalf("setup: CreateIndex() error = %v", err)
}
defer func() {
_ = DeleteIndex(ctx, c, indexName)
}()
// Create multiple documents
docs := []*Document{
{
ID: "doc1",
Source: map[string]any{
"title": "Document 1",
"value": 1,
},
},
{
ID: "doc2",
Source: map[string]any{
"title": "Document 2",
"value": 2,
},
},
{
ID: "doc3",
Source: map[string]any{
"title": "Document 3",
"value": 3,
},
},
}
err = BulkIndexDocuments(ctx, c, indexName, docs)
if err != nil {
t.Fatalf("BulkIndexDocuments() error = %v", err)
}
// Verify all documents were indexed
for _, doc := range docs {
retrieved, err := GetDocument(ctx, c, indexName, doc.ID)
if err != nil {
t.Errorf("GetDocument(%s) error = %v", doc.ID, err)
continue
}
if retrieved.Source["title"] != doc.Source["title"] {
t.Errorf("Document %s title = %v, want %v", doc.ID, retrieved.Source["title"], doc.Source["title"])
}
}
t.Logf("BulkIndexDocuments succeeded: indexed %d documents", len(docs))
}
func TestIndexDocument(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test in short mode")
}
c := setupIntegrationTest(t)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
indexName := "test-index-document"
// Cleanup any existing index first
_ = DeleteIndex(ctx, c, indexName)
// Setup
err := CreateIndex(ctx, c, indexName, nil)
if err != nil {
t.Fatalf("setup: CreateIndex() error = %v", err)
}
defer func() {
_ = DeleteIndex(ctx, c, indexName)
}()
doc := &Document{
Source: map[string]any{
"field1": "value1",
"field2": 123,
},
}
id, err := IndexDocument(ctx, c, indexName, doc)
if err != nil {
t.Fatalf("IndexDocument() error = %v", err)
}
if id == "" {
t.Error("IndexDocument() returned empty ID")
}
t.Logf("IndexDocument succeeded: %s", id)
}
func TestGetDocument(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test in short mode")
}
c := setupIntegrationTest(t)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
indexName := "test-get-document"
// Cleanup any existing index first
_ = DeleteIndex(ctx, c, indexName)
// Setup
err := CreateIndex(ctx, c, indexName, nil)
if err != nil {
t.Fatalf("setup: CreateIndex() error = %v", err)
}
defer func() {
_ = DeleteIndex(ctx, c, indexName)
}()
// Index a document
doc := &Document{
ID: "test-doc",
Source: map[string]any{
"test": "data",
},
}
_, err = IndexDocument(ctx, c, indexName, doc)
if err != nil {
t.Fatalf("setup: IndexDocument() error = %v", err)
}
// Test getting the document
retrieved, err := GetDocument(ctx, c, indexName, "test-doc")
if err != nil {
t.Fatalf("GetDocument() error = %v", err)
}
if retrieved.ID != "test-doc" {
t.Errorf("GetDocument() ID = %q, want %q", retrieved.ID, "test-doc")
}
t.Logf("GetDocument succeeded: %s", retrieved.ID)
}
func TestUpdateDocument(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test in short mode")
}
c := setupIntegrationTest(t)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
indexName := "test-update-document"
// Cleanup any existing index first
_ = DeleteIndex(ctx, c, indexName)
// Setup
err := CreateIndex(ctx, c, indexName, nil)
if err != nil {
t.Fatalf("setup: CreateIndex() error = %v", err)
}
defer func() {
_ = DeleteIndex(ctx, c, indexName)
}()
// Index a document
doc := &Document{
ID: "test-doc",
Source: map[string]any{
"value": 1,
},
}
_, err = IndexDocument(ctx, c, indexName, doc)
if err != nil {
t.Fatalf("setup: IndexDocument() error = %v", err)
}
// Test update
updates := map[string]any{
"value": 2,
}
err = UpdateDocument(ctx, c, indexName, "test-doc", updates)
if err != nil {
t.Fatalf("UpdateDocument() error = %v", err)
}
t.Log("UpdateDocument succeeded")
}
func TestDeleteDocument(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test in short mode")
}
c := setupIntegrationTest(t)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
indexName := "test-delete-document"
// Cleanup any existing index first
_ = DeleteIndex(ctx, c, indexName)
// Setup
err := CreateIndex(ctx, c, indexName, nil)
if err != nil {
t.Fatalf("setup: CreateIndex() error = %v", err)
}
defer func() {
_ = DeleteIndex(ctx, c, indexName)
}()
// Index a document
doc := &Document{
ID: "test-doc",
Source: map[string]any{
"test": "data",
},
}
_, err = IndexDocument(ctx, c, indexName, doc)
if err != nil {
t.Fatalf("setup: IndexDocument() error = %v", err)
}
// Test deletion
err = DeleteDocument(ctx, c, indexName, "test-doc")
if err != nil {
t.Fatalf("DeleteDocument() error = %v", err)
}
t.Log("DeleteDocument succeeded")
}

246
operations/index/index.go Normal file
View File

@@ -0,0 +1,246 @@
// Package index provides index-level operations for OpenSearch.
package index
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"es-demo/client"
"github.com/opensearch-project/opensearch-go/v2/opensearchapi"
)
var (
// ErrIndexNotFound is returned when the specified index does not exist.
ErrIndexNotFound = errors.New("index not found")
// ErrIndexAlreadyExists is returned when trying to create an index that already exists.
ErrIndexAlreadyExists = errors.New("index already exists")
// ErrInvalidIndexName is returned when the index name is invalid.
ErrInvalidIndexName = errors.New("invalid index name")
)
// IndexSettings represents the settings for an index.
type IndexSettings struct {
NumberOfShards int `json:"number_of_shards,omitempty"`
NumberOfReplicas int `json:"number_of_replicas,omitempty"`
}
// IndexConfig represents the complete configuration for creating an index.
type IndexConfig struct {
Settings *IndexSettings `json:"settings,omitempty"`
Mappings map[string]any `json:"mappings,omitempty"`
Aliases map[string]any `json:"aliases,omitempty"`
}
// IndexInfo represents information about an index.
type IndexInfo struct {
Name string
Settings *IndexSettings
Mappings map[string]any
Aliases map[string]any
}
// CreateIndex creates a new index with the specified configuration.
func CreateIndex(ctx context.Context, c *client.Client, name string, config *IndexConfig) error {
if name == "" {
return ErrInvalidIndexName
}
// Build request body
var body io.Reader
if config != nil {
data, err := json.Marshal(config)
if err != nil {
return fmt.Errorf("failed to marshal index config: %w", err)
}
body = bytes.NewReader(data)
}
req := opensearchapi.IndicesCreateRequest{
Index: name,
Body: body,
}
res, err := req.Do(ctx, c.GetClient())
if err != nil {
return fmt.Errorf("failed to execute create index request: %w", err)
}
defer func() {
if closeErr := res.Body.Close(); closeErr != nil {
fmt.Fprintf(os.Stderr, "warning: failed to close response body: %v\n", closeErr)
}
}()
if res.IsError() {
bodyBytes, _ := io.ReadAll(res.Body)
if res.StatusCode == 400 {
return fmt.Errorf("%w: %s", ErrIndexAlreadyExists, string(bodyBytes))
}
return fmt.Errorf("create index failed with status %s: %s", res.Status(), string(bodyBytes))
}
return nil
}
// GetIndex retrieves information about an index.
func GetIndex(ctx context.Context, c *client.Client, name string) (*IndexInfo, error) {
if name == "" {
return nil, ErrInvalidIndexName
}
req := opensearchapi.IndicesGetRequest{
Index: []string{name},
}
res, err := req.Do(ctx, c.GetClient())
if err != nil {
return nil, fmt.Errorf("failed to execute get index request: %w", err)
}
defer func() {
if closeErr := res.Body.Close(); closeErr != nil {
fmt.Fprintf(os.Stderr, "warning: failed to close response body: %v\n", closeErr)
}
}()
if res.StatusCode == 404 {
return nil, ErrIndexNotFound
}
if res.IsError() {
bodyBytes, _ := io.ReadAll(res.Body)
return nil, fmt.Errorf("get index failed with status %s: %s", res.Status(), string(bodyBytes))
}
// Parse response
var response map[string]struct {
Settings struct {
Index struct {
NumberOfShards string `json:"number_of_shards"`
NumberOfReplicas string `json:"number_of_replicas"`
} `json:"index"`
} `json:"settings"`
Mappings map[string]any `json:"mappings"`
Aliases map[string]any `json:"aliases"`
}
if err := json.NewDecoder(res.Body).Decode(&response); err != nil {
return nil, fmt.Errorf("failed to decode index info: %w", err)
}
indexData, exists := response[name]
if !exists {
return nil, ErrIndexNotFound
}
info := &IndexInfo{
Name: name,
Mappings: indexData.Mappings,
Aliases: indexData.Aliases,
}
return info, nil
}
// DeleteIndex deletes an index.
func DeleteIndex(ctx context.Context, c *client.Client, name string) error {
if name == "" {
return ErrInvalidIndexName
}
req := opensearchapi.IndicesDeleteRequest{
Index: []string{name},
}
res, err := req.Do(ctx, c.GetClient())
if err != nil {
return fmt.Errorf("failed to execute delete index request: %w", err)
}
defer func() {
if closeErr := res.Body.Close(); closeErr != nil {
fmt.Fprintf(os.Stderr, "warning: failed to close response body: %v\n", closeErr)
}
}()
if res.StatusCode == 404 {
return ErrIndexNotFound
}
if res.IsError() {
bodyBytes, _ := io.ReadAll(res.Body)
return fmt.Errorf("delete index failed with status %s: %s", res.Status(), string(bodyBytes))
}
return nil
}
// IndexExists checks if an index exists.
func IndexExists(ctx context.Context, c *client.Client, name string) (bool, error) {
if name == "" {
return false, ErrInvalidIndexName
}
req := opensearchapi.IndicesExistsRequest{
Index: []string{name},
}
res, err := req.Do(ctx, c.GetClient())
if err != nil {
return false, fmt.Errorf("failed to execute exists request: %w", err)
}
defer func() {
if closeErr := res.Body.Close(); closeErr != nil {
fmt.Fprintf(os.Stderr, "warning: failed to close response body: %v\n", closeErr)
}
}()
return res.StatusCode == 200, nil
}
// ListIndices lists all indices matching a pattern (use "*" for all).
func ListIndices(ctx context.Context, c *client.Client, pattern string) ([]string, error) {
if pattern == "" {
pattern = "*"
}
req := opensearchapi.CatIndicesRequest{
Index: []string{pattern},
Format: "json",
}
res, err := req.Do(ctx, c.GetClient())
if err != nil {
return nil, fmt.Errorf("failed to execute list indices request: %w", err)
}
defer func() {
if closeErr := res.Body.Close(); closeErr != nil {
fmt.Fprintf(os.Stderr, "warning: failed to close response body: %v\n", closeErr)
}
}()
if res.IsError() {
bodyBytes, _ := io.ReadAll(res.Body)
return nil, fmt.Errorf("list indices failed with status %s: %s", res.Status(), string(bodyBytes))
}
var indices []struct {
Index string `json:"index"`
}
if err := json.NewDecoder(res.Body).Decode(&indices); err != nil {
return nil, fmt.Errorf("failed to decode indices list: %w", err)
}
result := make([]string, len(indices))
for i, idx := range indices {
result[i] = idx.Index
}
return result, nil
}

View File

@@ -0,0 +1,297 @@
package index
import (
"context"
"testing"
"time"
)
// TestIndexIntegration tests complete index lifecycle.
func TestIndexIntegration(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test in short mode")
}
c := setupIntegrationTest(t)
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
indexName := "test-index-integration"
// Cleanup before and after test
_ = DeleteIndex(ctx, c, indexName)
defer func() {
_ = DeleteIndex(ctx, c, indexName)
}()
// Test 1: Create index
t.Run("CreateIndex", func(t *testing.T) {
config := &IndexConfig{
Settings: &IndexSettings{
NumberOfShards: 1,
NumberOfReplicas: 0,
},
Mappings: map[string]any{
"properties": map[string]any{
"title": map[string]any{
"type": "text",
},
"count": map[string]any{
"type": "integer",
},
},
},
}
err := CreateIndex(ctx, c, indexName, config)
if err != nil {
t.Fatalf("CreateIndex() error = %v", err)
}
t.Log("Index created successfully")
})
// Test 2: Index exists
t.Run("IndexExists", func(t *testing.T) {
exists, err := IndexExists(ctx, c, indexName)
if err != nil {
t.Fatalf("IndexExists() error = %v", err)
}
if !exists {
t.Error("IndexExists() = false, want true")
}
t.Log("Index exists check passed")
})
// Test 3: Get index
t.Run("GetIndex", func(t *testing.T) {
info, err := GetIndex(ctx, c, indexName)
if err != nil {
t.Fatalf("GetIndex() error = %v", err)
}
if info.Name != indexName {
t.Errorf("GetIndex() name = %q, want %q", info.Name, indexName)
}
t.Logf("Retrieved index: %s", info.Name)
})
// Test 4: List indices
t.Run("ListIndices", func(t *testing.T) {
indices, err := ListIndices(ctx, c, "test-*")
if err != nil {
t.Fatalf("ListIndices() error = %v", err)
}
found := false
for _, idx := range indices {
if idx == indexName {
found = true
break
}
}
if !found {
t.Errorf("Index %q not found in list", indexName)
}
t.Logf("Found %d indices", len(indices))
})
// Test 5: Delete index
t.Run("DeleteIndex", func(t *testing.T) {
err := DeleteIndex(ctx, c, indexName)
if err != nil {
t.Fatalf("DeleteIndex() error = %v", err)
}
// Verify deletion
exists, err := IndexExists(ctx, c, indexName)
if err != nil {
t.Fatalf("IndexExists() error = %v", err)
}
if exists {
t.Error("Index still exists after deletion")
}
t.Log("Index deleted successfully")
})
}
func TestCreateIndex(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test in short mode")
}
c := setupIntegrationTest(t)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
indexName := "test-create-index"
defer func() {
_ = DeleteIndex(ctx, c, indexName)
}()
config := &IndexConfig{
Settings: &IndexSettings{
NumberOfShards: 1,
NumberOfReplicas: 0,
},
}
err := CreateIndex(ctx, c, indexName, config)
if err != nil {
t.Fatalf("CreateIndex() error = %v", err)
}
t.Log("CreateIndex succeeded")
}
func TestGetIndex(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test in short mode")
}
c := setupIntegrationTest(t)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
indexName := "test-get-index"
// Cleanup any existing index first
_ = DeleteIndex(ctx, c, indexName)
// Setup
err := CreateIndex(ctx, c, indexName, nil)
if err != nil {
t.Fatalf("setup: CreateIndex() error = %v", err)
}
defer func() {
_ = DeleteIndex(ctx, c, indexName)
}()
// Test getting the index
info, err := GetIndex(ctx, c, indexName)
if err != nil {
t.Fatalf("GetIndex() error = %v", err)
}
if info.Name != indexName {
t.Errorf("GetIndex() name = %q, want %q", info.Name, indexName)
}
t.Logf("GetIndex succeeded: %s", info.Name)
}
func TestDeleteIndex(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test in short mode")
}
c := setupIntegrationTest(t)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
indexName := "test-delete-index"
// Create index
err := CreateIndex(ctx, c, indexName, nil)
if err != nil {
t.Fatalf("setup: CreateIndex() error = %v", err)
}
// Test deletion
err = DeleteIndex(ctx, c, indexName)
if err != nil {
t.Fatalf("DeleteIndex() error = %v", err)
}
t.Log("DeleteIndex succeeded")
}
func TestIndexExists(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test in short mode")
}
c := setupIntegrationTest(t)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
indexName := "test-exists-index"
// Test non-existent index
exists, err := IndexExists(ctx, c, indexName)
if err != nil {
t.Fatalf("IndexExists() error = %v", err)
}
if exists {
t.Error("IndexExists() = true for non-existent index")
}
// Create index
err = CreateIndex(ctx, c, indexName, nil)
if err != nil {
t.Fatalf("setup: CreateIndex() error = %v", err)
}
defer func() {
_ = DeleteIndex(ctx, c, indexName)
}()
// Test existing index
exists, err = IndexExists(ctx, c, indexName)
if err != nil {
t.Fatalf("IndexExists() error = %v", err)
}
if !exists {
t.Error("IndexExists() = false for existing index")
}
t.Log("IndexExists succeeded")
}
func TestListIndices(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test in short mode")
}
c := setupIntegrationTest(t)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
indices, err := ListIndices(ctx, c, "*")
if err != nil {
t.Fatalf("ListIndices() error = %v", err)
}
if indices == nil {
t.Error("ListIndices() returned nil")
}
t.Logf("ListIndices succeeded: found %d indices", len(indices))
}
// Unit tests for validation
func TestCreateIndex_Validation(t *testing.T) {
ctx := context.Background()
tests := []struct {
name string
indexName string
wantErr error
}{
{
name: "empty index name",
indexName: "",
wantErr: ErrInvalidIndexName,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Pass nil client to only test validation logic
err := CreateIndex(ctx, nil, tt.indexName, nil)
if err != tt.wantErr {
t.Errorf("CreateIndex() error = %v, want %v", err, tt.wantErr)
}
})
}
}

124
operations/index/mapping.go Normal file
View File

@@ -0,0 +1,124 @@
// Package index provides index-level operations for OpenSearch.
package index
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"os"
"es-demo/client"
"github.com/opensearch-project/opensearch-go/v2/opensearchapi"
)
// FieldMapping represents a field mapping configuration.
type FieldMapping struct {
Type string `json:"type"`
Fields map[string]any `json:"fields,omitempty"`
// Common field options
Index *bool `json:"index,omitempty"`
Store *bool `json:"store,omitempty"`
Analyzer string `json:"analyzer,omitempty"`
Format string `json:"format,omitempty"`
IgnoreAbove int `json:"ignore_above,omitempty"`
}
// PutMapping updates the mapping for an index by adding new fields.
// Note: Existing field mappings cannot be changed in OpenSearch.
func PutMapping(ctx context.Context, c *client.Client, indexName string, properties map[string]FieldMapping) error {
if indexName == "" {
return ErrInvalidIndexName
}
if len(properties) == 0 {
return fmt.Errorf("properties cannot be empty")
}
// Build mapping request
mappingBody := map[string]any{
"properties": properties,
}
data, err := json.Marshal(mappingBody)
if err != nil {
return fmt.Errorf("failed to marshal mapping: %w", err)
}
req := opensearchapi.IndicesPutMappingRequest{
Index: []string{indexName},
Body: bytes.NewReader(data),
}
res, err := req.Do(ctx, c.GetClient())
if err != nil {
return fmt.Errorf("failed to execute put mapping request: %w", err)
}
defer func() {
if closeErr := res.Body.Close(); closeErr != nil {
fmt.Fprintf(os.Stderr, "warning: failed to close response body: %v\n", closeErr)
}
}()
if res.IsError() {
bodyBytes, _ := io.ReadAll(res.Body)
return fmt.Errorf("put mapping failed with status %s: %s", res.Status(), string(bodyBytes))
}
return nil
}
// GetMapping retrieves the mapping for an index.
func GetMapping(ctx context.Context, c *client.Client, indexName string) (map[string]any, error) {
if indexName == "" {
return nil, ErrInvalidIndexName
}
req := opensearchapi.IndicesGetMappingRequest{
Index: []string{indexName},
}
res, err := req.Do(ctx, c.GetClient())
if err != nil {
return nil, fmt.Errorf("failed to execute get mapping request: %w", err)
}
defer func() {
if closeErr := res.Body.Close(); closeErr != nil {
fmt.Fprintf(os.Stderr, "warning: failed to close response body: %v\n", closeErr)
}
}()
if res.StatusCode == 404 {
return nil, ErrIndexNotFound
}
if res.IsError() {
bodyBytes, _ := io.ReadAll(res.Body)
return nil, fmt.Errorf("get mapping failed with status %s: %s", res.Status(), string(bodyBytes))
}
var response map[string]struct {
Mappings map[string]any `json:"mappings"`
}
if err := json.NewDecoder(res.Body).Decode(&response); err != nil {
return nil, fmt.Errorf("failed to decode mapping response: %w", err)
}
indexData, exists := response[indexName]
if !exists {
return nil, ErrIndexNotFound
}
return indexData.Mappings, nil
}
// AddField adds a new field to the index mapping.
// This is a convenience wrapper around PutMapping for adding a single field.
func AddField(ctx context.Context, c *client.Client, indexName string, fieldName string, mapping FieldMapping) error {
return PutMapping(ctx, c, indexName, map[string]FieldMapping{
fieldName: mapping,
})
}

View File

@@ -0,0 +1,204 @@
package index
import (
"context"
"testing"
"time"
)
// TestMappingIntegration tests mapping operations.
func TestMappingIntegration(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test in short mode")
}
c := setupIntegrationTest(t)
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
indexName := "test-mapping-integration"
// Setup index
err := CreateIndex(ctx, c, indexName, nil)
if err != nil {
t.Fatalf("setup: CreateIndex() error = %v", err)
}
defer func() {
_ = DeleteIndex(ctx, c, indexName)
}()
// Test 1: Add field
t.Run("AddField", func(t *testing.T) {
mapping := FieldMapping{
Type: "text",
}
err := AddField(ctx, c, indexName, "new_field", mapping)
if err != nil {
t.Fatalf("AddField() error = %v", err)
}
t.Log("Field added successfully")
})
// Test 2: Get mapping
t.Run("GetMapping", func(t *testing.T) {
mappings, err := GetMapping(ctx, c, indexName)
if err != nil {
t.Fatalf("GetMapping() error = %v", err)
}
if mappings == nil {
t.Error("GetMapping() returned nil")
}
t.Logf("Retrieved mappings for index %s", indexName)
})
// Test 3: Put mapping with multiple fields
t.Run("PutMapping", func(t *testing.T) {
properties := map[string]FieldMapping{
"title": {
Type: "text",
},
"count": {
Type: "integer",
},
"timestamp": {
Type: "date",
Format: "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis",
},
}
err := PutMapping(ctx, c, indexName, properties)
if err != nil {
t.Fatalf("PutMapping() error = %v", err)
}
t.Log("Mapping updated successfully")
})
}
func TestPutMapping(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test in short mode")
}
c := setupIntegrationTest(t)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
indexName := "test-put-mapping"
// Cleanup any existing index first
_ = DeleteIndex(ctx, c, indexName)
// Setup
err := CreateIndex(ctx, c, indexName, nil)
if err != nil {
t.Fatalf("setup: CreateIndex() error = %v", err)
}
defer func() {
_ = DeleteIndex(ctx, c, indexName)
}()
properties := map[string]FieldMapping{
"name": {
Type: "text",
},
"age": {
Type: "integer",
},
}
err = PutMapping(ctx, c, indexName, properties)
if err != nil {
t.Fatalf("PutMapping() error = %v", err)
}
t.Log("PutMapping succeeded")
}
func TestGetMapping(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test in short mode")
}
c := setupIntegrationTest(t)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
indexName := "test-get-mapping"
// Cleanup any existing index first
_ = DeleteIndex(ctx, c, indexName)
// Setup with initial mapping
config := &IndexConfig{
Mappings: map[string]any{
"properties": map[string]any{
"field1": map[string]any{
"type": "text",
},
},
},
}
err := CreateIndex(ctx, c, indexName, config)
if err != nil {
t.Fatalf("setup: CreateIndex() error = %v", err)
}
defer func() {
_ = DeleteIndex(ctx, c, indexName)
}()
mappings, err := GetMapping(ctx, c, indexName)
if err != nil {
t.Fatalf("GetMapping() error = %v", err)
}
if mappings == nil {
t.Error("GetMapping() returned nil")
}
t.Log("GetMapping succeeded")
}
func TestAddField(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test in short mode")
}
c := setupIntegrationTest(t)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
indexName := "test-add-field"
// Cleanup any existing index first
_ = DeleteIndex(ctx, c, indexName)
// Setup
err := CreateIndex(ctx, c, indexName, nil)
if err != nil {
t.Fatalf("setup: CreateIndex() error = %v", err)
}
defer func() {
_ = DeleteIndex(ctx, c, indexName)
}()
mapping := FieldMapping{
Type: "keyword",
}
err = AddField(ctx, c, indexName, "status", mapping)
if err != nil {
t.Fatalf("AddField() error = %v", err)
}
t.Log("AddField succeeded")
}

187
operations/index/search.go Normal file
View File

@@ -0,0 +1,187 @@
// Package index provides index-level operations for OpenSearch.
package index
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"os"
"es-demo/client"
"github.com/opensearch-project/opensearch-go/v2/opensearchapi"
)
// SearchQuery represents a search query configuration.
type SearchQuery struct {
// Query is the search query DSL
Query map[string]any `json:"query,omitempty"`
// Size is the maximum number of hits to return (default: 10)
Size int `json:"size,omitempty"`
// From is the starting offset (for pagination)
From int `json:"from,omitempty"`
// Sort defines the sort order
Sort []map[string]any `json:"sort,omitempty"`
// Source defines which fields to return
Source any `json:"_source,omitempty"`
}
// SearchResult represents search results.
type SearchResult struct {
Took int64 `json:"took"`
Hits struct {
Total struct {
Value int64 `json:"value"`
Relation string `json:"relation"`
} `json:"total"`
MaxScore *float64 `json:"max_score"`
Hits []Hit `json:"hits"`
} `json:"hits"`
}
// Hit represents a single search result hit.
type Hit struct {
Index string `json:"_index"`
ID string `json:"_id"`
Score *float64 `json:"_score"`
Source map[string]any `json:"_source"`
}
// Search performs a search query on an index.
func Search(ctx context.Context, c *client.Client, indexName string, query *SearchQuery) (*SearchResult, error) {
if indexName == "" {
return nil, ErrInvalidIndexName
}
if query == nil {
query = &SearchQuery{}
}
// Set defaults
if query.Size == 0 {
query.Size = 10
}
data, err := json.Marshal(query)
if err != nil {
return nil, fmt.Errorf("failed to marshal search query: %w", err)
}
req := opensearchapi.SearchRequest{
Index: []string{indexName},
Body: bytes.NewReader(data),
}
res, err := req.Do(ctx, c.GetClient())
if err != nil {
return nil, fmt.Errorf("failed to execute search request: %w", err)
}
defer func() {
if closeErr := res.Body.Close(); closeErr != nil {
fmt.Fprintf(os.Stderr, "warning: failed to close response body: %v\n", closeErr)
}
}()
if res.IsError() {
bodyBytes, _ := io.ReadAll(res.Body)
return nil, fmt.Errorf("search failed with status %s: %s", res.Status(), string(bodyBytes))
}
var result SearchResult
if err := json.NewDecoder(res.Body).Decode(&result); err != nil {
return nil, fmt.Errorf("failed to decode search result: %w", err)
}
return &result, nil
}
// MatchQuery creates a match query for full-text search.
func MatchQuery(field string, value any) map[string]any {
return map[string]any{
"match": map[string]any{
field: value,
},
}
}
// TermQuery creates a term query for exact matching.
func TermQuery(field string, value any) map[string]any {
return map[string]any{
"term": map[string]any{
field: value,
},
}
}
// RangeQuery creates a range query.
func RangeQuery(field string, gte, lte any) map[string]any {
rangeMap := make(map[string]any)
if gte != nil {
rangeMap["gte"] = gte
}
if lte != nil {
rangeMap["lte"] = lte
}
return map[string]any{
"range": map[string]any{
field: rangeMap,
},
}
}
// BoolQuery creates a bool query for combining multiple queries.
type BoolQuery struct {
Must []map[string]any `json:"must,omitempty"`
Should []map[string]any `json:"should,omitempty"`
MustNot []map[string]any `json:"must_not,omitempty"`
Filter []map[string]any `json:"filter,omitempty"`
}
// ToBoolQuery converts BoolQuery to query DSL.
func (b *BoolQuery) ToBoolQuery() map[string]any {
return map[string]any{
"bool": b,
}
}
// MatchAllQuery creates a match_all query.
func MatchAllQuery() map[string]any {
return map[string]any{
"match_all": map[string]any{},
}
}
// MultiMatchQuery creates a multi_match query for searching across multiple fields.
func MultiMatchQuery(value any, fields ...string) map[string]any {
return map[string]any{
"multi_match": map[string]any{
"query": value,
"fields": fields,
},
}
}
// WildcardQuery creates a wildcard query.
func WildcardQuery(field string, value string) map[string]any {
return map[string]any{
"wildcard": map[string]any{
field: value,
},
}
}
// PrefixQuery creates a prefix query.
func PrefixQuery(field string, value string) map[string]any {
return map[string]any{
"prefix": map[string]any{
field: value,
},
}
}

View File

@@ -0,0 +1,343 @@
package index
import (
"context"
"testing"
"time"
)
// TestSearchIntegration tests search operations.
func TestSearchIntegration(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test in short mode")
}
c := setupIntegrationTest(t)
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
indexName := "test-search-integration"
// Setup index with mapping
config := &IndexConfig{
Mappings: map[string]any{
"properties": map[string]any{
"title": map[string]any{
"type": "text",
},
"category": map[string]any{
"type": "keyword",
},
"price": map[string]any{
"type": "integer",
},
},
},
}
err := CreateIndex(ctx, c, indexName, config)
if err != nil {
t.Fatalf("setup: CreateIndex() error = %v", err)
}
defer func() {
_ = DeleteIndex(ctx, c, indexName)
}()
// Index test documents
docs := []*Document{
{
ID: "1",
Source: map[string]any{
"title": "Go Programming Language",
"category": "books",
"price": 50,
},
},
{
ID: "2",
Source: map[string]any{
"title": "Python for Beginners",
"category": "books",
"price": 40,
},
},
{
ID: "3",
Source: map[string]any{
"title": "JavaScript Guide",
"category": "books",
"price": 45,
},
},
}
err = BulkIndexDocuments(ctx, c, indexName, docs)
if err != nil {
t.Fatalf("setup: BulkIndexDocuments() error = %v", err)
}
// Give ES a moment to index
time.Sleep(1 * time.Second)
// Test 1: Match query
t.Run("MatchQuery", func(t *testing.T) {
query := &SearchQuery{
Query: MatchQuery("title", "Programming"),
Size: 10,
}
result, err := Search(ctx, c, indexName, query)
if err != nil {
t.Fatalf("Search() error = %v", err)
}
if result.Hits.Total.Value == 0 {
t.Error("Search() returned no results")
}
t.Logf("Match query found %d results", result.Hits.Total.Value)
})
// Test 2: Term query
t.Run("TermQuery", func(t *testing.T) {
query := &SearchQuery{
Query: TermQuery("category", "books"),
Size: 10,
}
result, err := Search(ctx, c, indexName, query)
if err != nil {
t.Fatalf("Search() error = %v", err)
}
if result.Hits.Total.Value != 3 {
t.Errorf("Term query found %d results, want 3", result.Hits.Total.Value)
}
t.Logf("Term query found %d results", result.Hits.Total.Value)
})
// Test 3: Range query
t.Run("RangeQuery", func(t *testing.T) {
query := &SearchQuery{
Query: RangeQuery("price", 40, 50),
Size: 10,
}
result, err := Search(ctx, c, indexName, query)
if err != nil {
t.Fatalf("Search() error = %v", err)
}
if result.Hits.Total.Value == 0 {
t.Error("Range query returned no results")
}
t.Logf("Range query found %d results", result.Hits.Total.Value)
})
// Test 4: Bool query
t.Run("BoolQuery", func(t *testing.T) {
boolQ := &BoolQuery{
Must: []map[string]any{
TermQuery("category", "books"),
},
Filter: []map[string]any{
RangeQuery("price", nil, 45),
},
}
query := &SearchQuery{
Query: boolQ.ToBoolQuery(),
Size: 10,
}
result, err := Search(ctx, c, indexName, query)
if err != nil {
t.Fatalf("Search() error = %v", err)
}
if result.Hits.Total.Value == 0 {
t.Error("Bool query returned no results")
}
t.Logf("Bool query found %d results", result.Hits.Total.Value)
})
// Test 5: Match all query
t.Run("MatchAllQuery", func(t *testing.T) {
query := &SearchQuery{
Query: MatchAllQuery(),
Size: 10,
}
result, err := Search(ctx, c, indexName, query)
if err != nil {
t.Fatalf("Search() error = %v", err)
}
if result.Hits.Total.Value != 3 {
t.Errorf("Match all query found %d results, want 3", result.Hits.Total.Value)
}
t.Logf("Match all query found %d results", result.Hits.Total.Value)
})
// Test 6: Pagination
t.Run("Pagination", func(t *testing.T) {
query := &SearchQuery{
Query: MatchAllQuery(),
Size: 1,
From: 1,
}
result, err := Search(ctx, c, indexName, query)
if err != nil {
t.Fatalf("Search() error = %v", err)
}
if len(result.Hits.Hits) != 1 {
t.Errorf("Pagination returned %d hits, want 1", len(result.Hits.Hits))
}
t.Log("Pagination test passed")
})
// Test 7: Multi-match query
t.Run("MultiMatchQuery", func(t *testing.T) {
query := &SearchQuery{
Query: MultiMatchQuery("Programming", "title", "category"),
Size: 10,
}
result, err := Search(ctx, c, indexName, query)
if err != nil {
t.Fatalf("Search() error = %v", err)
}
if result.Hits.Total.Value == 0 {
t.Error("Multi-match query returned no results")
}
t.Logf("Multi-match query found %d results", result.Hits.Total.Value)
})
}
func TestSearch(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test in short mode")
}
c := setupIntegrationTest(t)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
indexName := "test-search"
// Cleanup any existing index first
_ = DeleteIndex(ctx, c, indexName)
// Setup
err := CreateIndex(ctx, c, indexName, nil)
if err != nil {
t.Fatalf("setup: CreateIndex() error = %v", err)
}
defer func() {
_ = DeleteIndex(ctx, c, indexName)
}()
// Index a document
doc := &Document{
Source: map[string]any{
"message": "test search",
},
}
_, err = IndexDocument(ctx, c, indexName, doc)
if err != nil {
t.Fatalf("setup: IndexDocument() error = %v", err)
}
// Give ES a moment
time.Sleep(1 * time.Second)
query := &SearchQuery{
Query: MatchAllQuery(),
Size: 10,
}
result, err := Search(ctx, c, indexName, query)
if err != nil {
t.Fatalf("Search() error = %v", err)
}
if result == nil {
t.Error("Search() returned nil")
}
t.Logf("Search succeeded: found %d results", result.Hits.Total.Value)
}
func TestMatchQuery(t *testing.T) {
query := MatchQuery("field", "value")
if query == nil {
t.Error("MatchQuery() returned nil")
}
if _, ok := query["match"]; !ok {
t.Error("MatchQuery() missing 'match' key")
}
t.Log("MatchQuery test passed")
}
func TestTermQuery(t *testing.T) {
query := TermQuery("field", "value")
if query == nil {
t.Error("TermQuery() returned nil")
}
if _, ok := query["term"]; !ok {
t.Error("TermQuery() missing 'term' key")
}
t.Log("TermQuery test passed")
}
func TestRangeQuery(t *testing.T) {
query := RangeQuery("age", 18, 65)
if query == nil {
t.Error("RangeQuery() returned nil")
}
if _, ok := query["range"]; !ok {
t.Error("RangeQuery() missing 'range' key")
}
t.Log("RangeQuery test passed")
}
func TestBoolQuery(t *testing.T) {
boolQ := &BoolQuery{
Must: []map[string]any{
MatchQuery("field", "value"),
},
}
query := boolQ.ToBoolQuery()
if query == nil {
t.Error("ToBoolQuery() returned nil")
}
if _, ok := query["bool"]; !ok {
t.Error("ToBoolQuery() missing 'bool' key")
}
t.Log("BoolQuery test passed")
}