Skip to content

Commit 5fa0051

Browse files
committed
[IMPROVEMENT]: Add collection initialization, bulk data import, and search enhancements to Gin implementation
1 parent f4bac6c commit 5fa0051

5 files changed

Lines changed: 211 additions & 7 deletions

File tree

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,4 +53,7 @@ pnpm-lock.yaml
5353
.astro/
5454

5555
# react-native
56-
.expo
56+
.expo
57+
58+
# Data
59+
books.jsonl

typesense-gin-full-text-search/routes/search.go

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,10 @@ func searchBooks(c *gin.Context) {
2727

2828
// Create search parameters
2929
searchParams := &api.SearchCollectionParams{
30-
Q: pointer.String(query),
31-
QueryBy: pointer.String("title,authors"),
30+
Q: pointer.String(query),
31+
QueryBy: pointer.String("title,authors"),
32+
QueryByWeights: pointer.String("2,1"), // Title matches are weighted 2x more than author matches
33+
FacetBy: pointer.String("authors,publication_year,average_rating"), // Get facet counts for filtering
3234
}
3335

3436
// Perform search using the Typesense client
@@ -47,9 +49,10 @@ func searchBooks(c *gin.Context) {
4749

4850
// Return search results
4951
c.JSON(http.StatusOK, gin.H{
50-
"query": query,
51-
"results": *result.Hits,
52-
"found": *result.Found,
53-
"took": result.SearchTimeMs,
52+
"query": query,
53+
"results": *result.Hits,
54+
"found": *result.Found,
55+
"took": result.SearchTimeMs,
56+
"facet_counts": result.FacetCounts,
5457
})
5558
}

typesense-gin-full-text-search/server.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,33 @@
11
package main
22

33
import (
4+
"context"
5+
"log"
6+
"time"
7+
48
"github.com/gin-contrib/cors"
59
"github.com/gin-gonic/gin"
610
"github.com/typesense/code-samples/typesense-gin-full-text-search/routes"
711
"github.com/typesense/code-samples/typesense-gin-full-text-search/utils"
812
)
913

1014
func main() {
15+
// Initialize collections before starting the server
16+
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
17+
defer cancel()
18+
19+
if err := utils.InitializeCollections(ctx); err != nil {
20+
log.Fatalf("Failed to initialize collections: %v", err)
21+
}
22+
23+
// Initialize data if collection is empty
24+
// This is idempotent - only imports if collection has no documents
25+
dataFile := "books.jsonl"
26+
if err := utils.InitializeDataIfEmpty(ctx, utils.BookCollection, dataFile); err != nil {
27+
log.Printf("Warning: Failed to initialize data: %v", err)
28+
log.Println("Server will continue, but collection may be empty")
29+
}
30+
1131
router := gin.Default()
1232

1333
// CORS middleware
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package utils
2+
3+
import (
4+
"context"
5+
"log"
6+
7+
"github.com/typesense/typesense-go/v4/typesense/api"
8+
"github.com/typesense/typesense-go/v4/typesense/api/pointer"
9+
)
10+
11+
// InitializeCollections ensures all required collections exist
12+
// This is idempotent - safe to call multiple times
13+
func InitializeCollections(ctx context.Context) error {
14+
log.Println("Initializing Typesense collections...")
15+
16+
// Define the books collection schema
17+
booksSchema := &api.CollectionSchema{
18+
Name: BookCollection,
19+
Fields: []api.Field{
20+
{Name: "title", Type: "string", Facet: pointer.False()},
21+
{Name: "authors", Type: "string[]", Facet: pointer.True()},
22+
{Name: "publication_year", Type: "int32", Facet: pointer.True()},
23+
{Name: "average_rating", Type: "float", Facet: pointer.True()},
24+
{Name: "image_url", Type: "string", Facet: pointer.False()},
25+
{Name: "ratings_count", Type: "int32", Facet: pointer.True()},
26+
},
27+
DefaultSortingField: pointer.String("ratings_count"),
28+
}
29+
30+
// Try to retrieve the collection to check if it exists
31+
_, err := Client.Collection(BookCollection).Retrieve(ctx)
32+
if err != nil {
33+
// Collection doesn't exist, create it
34+
log.Printf("Collection '%s' not found, creating...", BookCollection)
35+
_, err = Client.Collections().Create(ctx, booksSchema)
36+
if err != nil {
37+
log.Printf("Failed to create collection '%s': %v", BookCollection, err)
38+
return err
39+
}
40+
log.Printf("Collection '%s' created successfully", BookCollection)
41+
} else {
42+
log.Printf("Collection '%s' already exists, skipping creation", BookCollection)
43+
}
44+
45+
return nil
46+
}
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
package utils
2+
3+
import (
4+
"bufio"
5+
"context"
6+
"encoding/json"
7+
"fmt"
8+
"log"
9+
"os"
10+
11+
"github.com/typesense/typesense-go/v4/typesense/api"
12+
"github.com/typesense/typesense-go/v4/typesense/api/pointer"
13+
)
14+
15+
// ImportDocumentsFromJSONL imports documents from a JSONL file in bulk
16+
// This is the production-ready way to load initial data
17+
func ImportDocumentsFromJSONL(ctx context.Context, collectionName, filePath string) error {
18+
log.Printf("Starting bulk import from %s to collection '%s'...", filePath, collectionName)
19+
20+
// Read the JSONL file
21+
file, err := os.Open(filePath)
22+
if err != nil {
23+
return fmt.Errorf("failed to open file: %w", err)
24+
}
25+
defer file.Close()
26+
27+
// Parse each line as a JSON document
28+
scanner := bufio.NewScanner(file)
29+
var documents []interface{}
30+
lineCount := 0
31+
32+
for scanner.Scan() {
33+
var doc map[string]interface{}
34+
if err := json.Unmarshal(scanner.Bytes(), &doc); err != nil {
35+
log.Printf("Warning: skipping invalid JSON line: %v", err)
36+
continue
37+
}
38+
documents = append(documents, doc)
39+
lineCount++
40+
}
41+
42+
if err := scanner.Err(); err != nil {
43+
return fmt.Errorf("error reading file: %w", err)
44+
}
45+
46+
log.Printf("Read %d documents from file", lineCount)
47+
48+
// Import documents in bulk using the import API
49+
// BatchSize controls how many documents are processed at once
50+
importParams := &api.ImportDocumentsParams{
51+
BatchSize: pointer.Int(100), // Process in batches of 100
52+
}
53+
54+
// The Import method accepts []interface{} containing document maps
55+
results, err := Client.Collection(collectionName).Documents().Import(
56+
ctx,
57+
documents,
58+
importParams,
59+
)
60+
61+
if err != nil {
62+
return fmt.Errorf("bulk import failed: %w", err)
63+
}
64+
65+
// Count successes and failures
66+
successCount := 0
67+
failureCount := 0
68+
69+
for _, result := range results {
70+
if result.Success {
71+
successCount++
72+
} else {
73+
failureCount++
74+
// Log first few errors for debugging
75+
if failureCount <= 5 {
76+
log.Printf("Import error: %s", result.Error)
77+
}
78+
}
79+
}
80+
81+
log.Printf("Bulk import completed: %d succeeded, %d failed", successCount, failureCount)
82+
83+
if failureCount > 0 && failureCount > lineCount/2 {
84+
// Only error if more than half failed
85+
return fmt.Errorf("bulk import had too many failures: %d out of %d", failureCount, lineCount)
86+
}
87+
88+
return nil
89+
}
90+
91+
// CheckCollectionDocumentCount returns the number of documents in a collection
92+
func CheckCollectionDocumentCount(ctx context.Context, collectionName string) (int, error) {
93+
collection, err := Client.Collection(collectionName).Retrieve(ctx)
94+
if err != nil {
95+
return 0, fmt.Errorf("failed to retrieve collection: %w", err)
96+
}
97+
98+
return int(*collection.NumDocuments), nil
99+
}
100+
101+
// InitializeDataIfEmpty checks if collection is empty and imports data if needed
102+
// This is idempotent - safe to run on every startup
103+
func InitializeDataIfEmpty(ctx context.Context, collectionName, dataFilePath string) error {
104+
log.Printf("Checking if collection '%s' needs data initialization...", collectionName)
105+
106+
// Check current document count
107+
count, err := CheckCollectionDocumentCount(ctx, collectionName)
108+
if err != nil {
109+
return fmt.Errorf("failed to check document count: %w", err)
110+
}
111+
112+
if count > 0 {
113+
log.Printf("Collection '%s' already has %d documents, skipping import", collectionName, count)
114+
return nil
115+
}
116+
117+
log.Printf("Collection '%s' is empty, importing data from %s", collectionName, dataFilePath)
118+
119+
// Import data
120+
if err := ImportDocumentsFromJSONL(ctx, collectionName, dataFilePath); err != nil {
121+
return fmt.Errorf("failed to import data: %w", err)
122+
}
123+
124+
// Verify import
125+
newCount, err := CheckCollectionDocumentCount(ctx, collectionName)
126+
if err != nil {
127+
return fmt.Errorf("failed to verify import: %w", err)
128+
}
129+
130+
log.Printf("Data import successful: collection '%s' now has %d documents", collectionName, newCount)
131+
return nil
132+
}

0 commit comments

Comments
 (0)