Skip to content

Commit aca6d32

Browse files
authored
Merge pull request #14 from nmdra/9-full-text-search-endpoint
9 full text search endpoint
2 parents e573c8a + 57bbd5f commit aca6d32

File tree

9 files changed

+125
-9
lines changed

9 files changed

+125
-9
lines changed

README.md

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -58,14 +58,24 @@ C4Context
5858
5959
### API Endpoints
6060
61-
* **`POST /books`**
62-
Add a new book by providing its title and description. The service generates and stores a semantic embedding of the description in the database.
61+
#### `POST /books`
6362
64-
* **`GET /search?q=your+query`**
65-
Perform a semantic search on the stored books using the query text. Returns a list of matching books ranked by relevance.
63+
Add a new book by providing its title, description, and ISBN.
64+
The service generates and stores a **semantic embedding** and full-text index.
6665
67-
* **`GET /ping`**
68-
Simple health check endpoint to verify if the service is running.
66+
#### `GET /search/vector?q=your+query`
67+
68+
Perform a **semantic search** on stored books using vector similarity with the query.
69+
Returns books ranked by **cosine similarity** of embeddings.
70+
71+
#### `GET /search/text?q=your+query`
72+
73+
Perform a **full-text search** using PostgreSQL’s full-text index on title and description.
74+
Returns books ranked by **textual relevance (ts\_rank)**.
75+
76+
#### `GET /ping`
77+
78+
Health check endpoint to verify if the service is running.
6979
7080
#### Example Usage
7181

api/handlers.go

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ type AddBookRequest struct {
1717
Isbn string `json:"isbn"`
1818
}
1919

20+
// POST /books
2021
func (h *BookHandler) AddBook(c echo.Context) error {
2122
ctx := c.Request().Context()
2223
var req AddBookRequest
@@ -30,14 +31,15 @@ func (h *BookHandler) AddBook(c echo.Context) error {
3031
default:
3132
}
3233

33-
err := h.Service.AddBook(c.Request().Context(), req.Isbn, req.Title, req.Description)
34+
err := h.Service.AddBook(ctx, req.Isbn, req.Title, req.Description)
3435
if err != nil {
3536
return c.JSON(http.StatusInternalServerError, echo.Map{"error": err.Error()})
3637
}
3738

3839
return c.JSON(http.StatusCreated, echo.Map{"status": "book added"})
3940
}
4041

42+
// GET /search/vector?q=
4143
func (h *BookHandler) SearchBooks(c echo.Context) error {
4244
ctx := c.Request().Context()
4345
query := c.QueryParam("q")
@@ -61,3 +63,28 @@ func (h *BookHandler) SearchBooks(c echo.Context) error {
6163

6264
return c.JSON(http.StatusOK, results)
6365
}
66+
67+
// GET /search/text?q=
68+
func (h *BookHandler) FullTextSearch(c echo.Context) error {
69+
ctx := c.Request().Context()
70+
query := c.QueryParam("q")
71+
if query == "" {
72+
return c.JSON(http.StatusBadRequest, echo.Map{"error": "missing query"})
73+
}
74+
75+
select {
76+
case <-ctx.Done():
77+
return c.JSON(http.StatusRequestTimeout, echo.Map{"error": "request canceled or timed out"})
78+
default:
79+
}
80+
81+
results, err := h.Service.FullTextSearch(ctx, query)
82+
if err != nil {
83+
if ctx.Err() != nil {
84+
return c.JSON(http.StatusRequestTimeout, echo.Map{"error": "request timed out"})
85+
}
86+
return c.JSON(http.StatusInternalServerError, echo.Map{"error": err.Error()})
87+
}
88+
89+
return c.JSON(http.StatusOK, results)
90+
}

cmd/main.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,8 @@ func main() {
9898
e.GET("/ping", func(c echo.Context) error {
9999
return c.String(200, "pong")
100100
})
101-
e.GET("/search", bookHandler.SearchBooks)
101+
e.GET("/search/semantic", bookHandler.SearchBooks)
102+
e.GET("/search/text", bookHandler.FullTextSearch)
102103
e.POST("/books", bookHandler.AddBook)
103104

104105
// e.Logger.Fatal(e.Start(fmt.Sprintf(":%d", cfg.port)))

db/books.sql

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,11 @@ LIMIT 5;
1111
-- name: GetBookByISBN :one
1212
SELECT id, isbn
1313
FROM books
14-
WHERE isbn = $1;
14+
WHERE isbn = $1;
15+
16+
-- name: SearchBooksByText :many
17+
SELECT id, isbn, title, description
18+
FROM books
19+
WHERE tsv @@ plainto_tsquery('english', $1)
20+
ORDER BY ts_rank(tsv, plainto_tsquery('english', $1)) DESC
21+
LIMIT 10;
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
DROP INDEX IF EXISTS idx_books_tsv;
2+
3+
ALTER TABLE books
4+
DROP COLUMN IF EXISTS tsv;
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
ALTER TABLE books
2+
ADD COLUMN tsv tsvector GENERATED ALWAYS AS (
3+
to_tsvector('english', coalesce(title, '') || ' ' || coalesce(description, ''))
4+
) STORED;
5+
6+
CREATE INDEX idx_books_tsv ON books USING GIN (tsv);

internal/repository/books.sql.go

Lines changed: 40 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/repository/models.go

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/service/book_service.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,3 +124,23 @@ func cosineSimilarity(a, b []float32) (float64, error) {
124124

125125
return dot / (math.Sqrt(normA) * math.Sqrt(normB)), nil
126126
}
127+
128+
func (s *BookService) FullTextSearch(ctx context.Context, query string) ([]repository.SearchBooksByTextRow, error) {
129+
select {
130+
case <-ctx.Done():
131+
return nil, ctx.Err()
132+
default:
133+
}
134+
135+
if query == "" {
136+
return nil, fmt.Errorf("search query cannot be empty")
137+
}
138+
139+
books, err := s.Repository.SearchBooksByText(ctx, query)
140+
if err != nil {
141+
s.Logger.Error("Full-text search failed", "query", query, "error", err)
142+
return nil, fmt.Errorf("full-text search failed: %w", err)
143+
}
144+
145+
return books, nil
146+
}

0 commit comments

Comments
 (0)