diff --git a/.gitignore b/.gitignore index a906e02..929d39f 100644 --- a/.gitignore +++ b/.gitignore @@ -53,4 +53,7 @@ pnpm-lock.yaml .astro/ # react-native -.expo \ No newline at end of file +.expo + +# Data +books.jsonl \ No newline at end of file diff --git a/typesense-gin-full-text-search/.env.example b/typesense-gin-full-text-search/.env.example new file mode 100644 index 0000000..d8b0936 --- /dev/null +++ b/typesense-gin-full-text-search/.env.example @@ -0,0 +1,9 @@ +# Server Configuration +PORT=3000 + +# Typesense Configuration +TYPESENSE_HOST=localhost +TYPESENSE_PORT=8108 +TYPESENSE_PROTOCOL=http +TYPESENSE_API_KEY=xyz +TYPESENSE_COLLECTION=books diff --git a/typesense-gin-full-text-search/README.md b/typesense-gin-full-text-search/README.md new file mode 100644 index 0000000..c256d50 --- /dev/null +++ b/typesense-gin-full-text-search/README.md @@ -0,0 +1,120 @@ +# Gin Full-Text Search with Typesense + +A RESTful search API built with Go Gin framework and Typesense, featuring full-text search capabilities with environment-based configuration. + +## Tech Stack + +- Go 1.19+ +- Gin Web Framework +- Typesense + +## Prerequisites + +- Go 1.19+ installed +- Docker (for running Typesense locally). Alternatively, you can use a Typesense Cloud cluster. +- Basic knowledge of Go and REST APIs. + +## Quick Start + +### 1. Clone the repository + +```bash +git clone https://github.com/typesense/code-samples.git +cd typesense-gin-full-text-search +``` + +### 2. Install dependencies + +```bash +go mod download +``` + +### 3. Set up environment variables + +Create a `.env` file in the project root with the following content: + +```env +# Server Configuration +PORT=3000 + +# Typesense Configuration +TYPESENSE_HOST=localhost +TYPESENSE_PORT=8108 +TYPESENSE_PROTOCOL=http +TYPESENSE_API_KEY=xyz +TYPESENSE_COLLECTION=books +``` + +### 4. Project Structure + +```text +├── routes +│ └── search.go +├── utils +│ ├── env.go +│ └── typesense.go +├── server.go +├── go.mod +└── .env +``` + +### 5. Start the development server + +**Standard mode:** + +```bash +go run server.go +``` + +**Hot reload mode (recommended for development):** + +First, install CompileDaemon: + +```bash +go install github.com/githubnemo/CompileDaemon@latest +``` + +Then run: + +```bash +CompileDaemon --build="go build -o server ." --command="./server" +``` + +The server will automatically restart when you make changes to any Go file. + +Open [http://localhost:3000](http://localhost:3000) in your browser. + +### 6. Search API Endpoint + +**Search:** + +```bash +GET /search?q= +``` + +Example: + +```bash +curl "http://localhost:3000/search?q=harry" +``` + +### 7. Deployment + +Set env variables to point the app to the Typesense Cluster: + +```env +# Server Configuration +PORT=3000 + +# Typesense Configuration +TYPESENSE_HOST=xxx.typesense.net +TYPESENSE_PORT=443 +TYPESENSE_PROTOCOL=https +TYPESENSE_API_KEY=your-production-api-key +TYPESENSE_COLLECTION=books +``` + +- Configure CORS middleware for specific origins. +- Configure gin to run in release mode. +- Add some sort of authentication to the API. +- Add rate limiting to the API. diff --git a/typesense-gin-full-text-search/go.mod b/typesense-gin-full-text-search/go.mod new file mode 100644 index 0000000..d1c8348 --- /dev/null +++ b/typesense-gin-full-text-search/go.mod @@ -0,0 +1,51 @@ +module github.com/typesense/code-samples/typesense-gin-full-text-search + +go 1.25.0 + +require ( + github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect + github.com/bytedance/gopkg v0.1.3 // indirect + github.com/bytedance/sonic v1.15.0 // indirect + github.com/bytedance/sonic/loader v0.5.0 // indirect + github.com/cloudwego/base64x v0.1.6 // indirect + github.com/fatih/color v1.9.0 // indirect + github.com/fsnotify/fsnotify v1.4.9 // indirect + github.com/gabriel-vasile/mimetype v1.4.13 // indirect + github.com/gin-contrib/cors v1.7.6 // indirect + github.com/gin-contrib/sse v1.1.0 // indirect + github.com/gin-gonic/gin v1.11.0 // indirect + github.com/githubnemo/CompileDaemon v1.4.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.30.1 // indirect + github.com/goccy/go-json v0.10.5 // indirect + github.com/goccy/go-yaml v1.19.2 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/joho/godotenv v1.5.1 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/oapi-codegen/runtime v1.1.1 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/quic-go/qpack v0.6.0 // indirect + github.com/quic-go/quic-go v0.59.0 // indirect + github.com/radovskyb/watcher v1.0.7 // indirect + github.com/sony/gobreaker v1.0.0 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/typesense/typesense-go/v4 v4.0.0-alpha2 // indirect + github.com/ugorji/go/codec v1.3.1 // indirect + go.uber.org/mock v0.6.0 // indirect + golang.org/x/arch v0.24.0 // indirect + golang.org/x/crypto v0.48.0 // indirect + golang.org/x/mod v0.33.0 // indirect + golang.org/x/net v0.50.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.41.0 // indirect + golang.org/x/text v0.34.0 // indirect + golang.org/x/tools v0.42.0 // indirect + google.golang.org/protobuf v1.36.11 // indirect +) diff --git a/typesense-gin-full-text-search/go.sum b/typesense-gin-full-text-search/go.sum new file mode 100644 index 0000000..07e438c --- /dev/null +++ b/typesense-gin-full-text-search/go.sum @@ -0,0 +1,122 @@ +github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= +github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= +github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= +github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= +github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= +github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= +github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE= +github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k= +github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE= +github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= +github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= +github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s= +github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM= +github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= +github.com/gin-contrib/cors v1.7.6 h1:3gQ8GMzs1Ylpf70y8bMw4fVpycXIeX1ZemuSQIsnQQY= +github.com/gin-contrib/cors v1.7.6/go.mod h1:Ulcl+xN4jel9t1Ry8vqph23a60FwH9xVLd+3ykmTjOk= +github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= +github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= +github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk= +github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls= +github.com/githubnemo/CompileDaemon v1.4.0 h1:z96Qu4tj+RzRfF+L7f1O6E8ion5JQlisWeXWc2wzwDQ= +github.com/githubnemo/CompileDaemon v1.4.0/go.mod h1:/G125r3YBIp6rcXtCZfiEHwFzcl7GSsNSwylxSNrkMA= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w= +github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= +github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM= +github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/oapi-codegen/runtime v1.1.1 h1:EXLHh0DXIJnWhdRPN2w4MXAzFyE4CskzhNLUmtpMYro= +github.com/oapi-codegen/runtime v1.1.1/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= +github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= +github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw= +github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= +github.com/radovskyb/watcher v1.0.7 h1:AYePLih6dpmS32vlHfhCeli8127LzkIgwJGcwwe8tUE= +github.com/radovskyb/watcher v1.0.7/go.mod h1:78okwvY5wPdzcb1UYnip1pvrZNIVEIh/Cm+ZuvsUYIg= +github.com/sony/gobreaker v1.0.0 h1:feX5fGGXSl3dYd4aHZItw+FpHLvvoaqkawKjVNiFMNQ= +github.com/sony/gobreaker v1.0.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= +github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/typesense/typesense-go/v4 v4.0.0-alpha2 h1:L9icvEu+N9ARlmwh0eM3Cfc/zZLTNwn5nXS1Z4bztEk= +github.com/typesense/typesense-go/v4 v4.0.0-alpha2/go.mod h1:Y880M+mG3T9jthku5MJBmfXrrc2wyE6ZotLOOADZx9Q= +github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY= +github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= +go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= +go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= +golang.org/x/arch v0.24.0 h1:qlJ3M9upxvFfwRM51tTg3Yl+8CP9vCC1E7vlFpgv99Y= +golang.org/x/arch v0.24.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= +golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= +golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= +golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= +golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= +golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= +golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= +golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/typesense-gin-full-text-search/routes/search.go b/typesense-gin-full-text-search/routes/search.go new file mode 100644 index 0000000..dec7428 --- /dev/null +++ b/typesense-gin-full-text-search/routes/search.go @@ -0,0 +1,58 @@ +package routes + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "github.com/typesense/code-samples/typesense-gin-full-text-search/utils" + "github.com/typesense/typesense-go/v4/typesense/api" + "github.com/typesense/typesense-go/v4/typesense/api/pointer" +) + +// SetupSearchRoutes configures all search-related routes +func SetupSearchRoutes(router *gin.Engine) { + // Simple search endpoint + router.GET("/search", searchBooks) +} + +// searchBooks handles the search request +func searchBooks(c *gin.Context) { + query := c.Query("q") + if query == "" { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "Search query parameter 'q' is required", + }) + return + } + + // Create search parameters + searchParams := &api.SearchCollectionParams{ + Q: pointer.String(query), + QueryBy: pointer.String("title,authors"), + QueryByWeights: pointer.String("2,1"), // Title matches are weighted 2x more than author matches + FacetBy: pointer.String("authors,publication_year,average_rating"), // Get facet counts for filtering + } + + // Perform search using the Typesense client + result, err := utils.Client.Collection(utils.BookCollection).Documents().Search(c.Request.Context(), searchParams) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Search failed: " + err.Error(), + "debug": gin.H{ + "collection": utils.BookCollection, + "query": query, + "server_url": utils.GetServerURL(), + }, + }) + return + } + + // Return search results + c.JSON(http.StatusOK, gin.H{ + "query": query, + "results": *result.Hits, + "found": *result.Found, + "took": result.SearchTimeMs, + "facet_counts": result.FacetCounts, + }) +} diff --git a/typesense-gin-full-text-search/server.go b/typesense-gin-full-text-search/server.go new file mode 100644 index 0000000..b85c828 --- /dev/null +++ b/typesense-gin-full-text-search/server.go @@ -0,0 +1,54 @@ +package main + +import ( + "context" + "log" + "time" + + "github.com/gin-contrib/cors" + "github.com/gin-gonic/gin" + "github.com/typesense/code-samples/typesense-gin-full-text-search/routes" + "github.com/typesense/code-samples/typesense-gin-full-text-search/utils" +) + +func main() { + // Initialize collections before starting the server + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + if err := utils.InitializeCollections(ctx); err != nil { + log.Fatalf("Failed to initialize collections: %v", err) + } + + // Initialize data if collection is empty + // This is idempotent - only imports if collection has no documents + dataFile := "books.jsonl" + if err := utils.InitializeDataIfEmpty(ctx, utils.BookCollection, dataFile); err != nil { + log.Printf("Warning: Failed to initialize data: %v", err) + log.Println("Server will continue, but collection may be empty") + } + + router := gin.Default() + + // CORS middleware + router.Use(cors.New(cors.Config{ + AllowOrigins: []string{"*"}, + AllowMethods: []string{"GET", "OPTIONS"}, + AllowHeaders: []string{"Origin", "Content-Type", "Accept", "Authorization"}, + ExposeHeaders: []string{"Content-Length"}, + AllowCredentials: true, + })) + + // Health check endpoint + router.GET("/ping", func(c *gin.Context) { + c.JSON(200, gin.H{ + "message": "pong", + }) + }) + + // Setup search routes + routes.SetupSearchRoutes(router) + + port := utils.GetEnv("PORT", "3000") + router.Run(":" + port) +} diff --git a/typesense-gin-full-text-search/utils/collections.go b/typesense-gin-full-text-search/utils/collections.go new file mode 100644 index 0000000..4186ce8 --- /dev/null +++ b/typesense-gin-full-text-search/utils/collections.go @@ -0,0 +1,46 @@ +package utils + +import ( + "context" + "log" + + "github.com/typesense/typesense-go/v4/typesense/api" + "github.com/typesense/typesense-go/v4/typesense/api/pointer" +) + +// InitializeCollections ensures all required collections exist +// This is idempotent - safe to call multiple times +func InitializeCollections(ctx context.Context) error { + log.Println("Initializing Typesense collections...") + + // Define the books collection schema + booksSchema := &api.CollectionSchema{ + Name: BookCollection, + Fields: []api.Field{ + {Name: "title", Type: "string", Facet: pointer.False()}, + {Name: "authors", Type: "string[]", Facet: pointer.True()}, + {Name: "publication_year", Type: "int32", Facet: pointer.True()}, + {Name: "average_rating", Type: "float", Facet: pointer.True()}, + {Name: "image_url", Type: "string", Facet: pointer.False()}, + {Name: "ratings_count", Type: "int32", Facet: pointer.True()}, + }, + DefaultSortingField: pointer.String("ratings_count"), + } + + // Try to retrieve the collection to check if it exists + _, err := Client.Collection(BookCollection).Retrieve(ctx) + if err != nil { + // Collection doesn't exist, create it + log.Printf("Collection '%s' not found, creating...", BookCollection) + _, err = Client.Collections().Create(ctx, booksSchema) + if err != nil { + log.Printf("Failed to create collection '%s': %v", BookCollection, err) + return err + } + log.Printf("Collection '%s' created successfully", BookCollection) + } else { + log.Printf("Collection '%s' already exists, skipping creation", BookCollection) + } + + return nil +} diff --git a/typesense-gin-full-text-search/utils/data_import.go b/typesense-gin-full-text-search/utils/data_import.go new file mode 100644 index 0000000..c71c369 --- /dev/null +++ b/typesense-gin-full-text-search/utils/data_import.go @@ -0,0 +1,132 @@ +package utils + +import ( + "bufio" + "context" + "encoding/json" + "fmt" + "log" + "os" + + "github.com/typesense/typesense-go/v4/typesense/api" + "github.com/typesense/typesense-go/v4/typesense/api/pointer" +) + +// ImportDocumentsFromJSONL imports documents from a JSONL file in bulk +// This is the production-ready way to load initial data +func ImportDocumentsFromJSONL(ctx context.Context, collectionName, filePath string) error { + log.Printf("Starting bulk import from %s to collection '%s'...", filePath, collectionName) + + // Read the JSONL file + file, err := os.Open(filePath) + if err != nil { + return fmt.Errorf("failed to open file: %w", err) + } + defer file.Close() + + // Parse each line as a JSON document + scanner := bufio.NewScanner(file) + var documents []interface{} + lineCount := 0 + + for scanner.Scan() { + var doc map[string]interface{} + if err := json.Unmarshal(scanner.Bytes(), &doc); err != nil { + log.Printf("Warning: skipping invalid JSON line: %v", err) + continue + } + documents = append(documents, doc) + lineCount++ + } + + if err := scanner.Err(); err != nil { + return fmt.Errorf("error reading file: %w", err) + } + + log.Printf("Read %d documents from file", lineCount) + + // Import documents in bulk using the import API + // BatchSize controls how many documents are processed at once + importParams := &api.ImportDocumentsParams{ + BatchSize: pointer.Int(100), // Process in batches of 100 + } + + // The Import method accepts []interface{} containing document maps + results, err := Client.Collection(collectionName).Documents().Import( + ctx, + documents, + importParams, + ) + + if err != nil { + return fmt.Errorf("bulk import failed: %w", err) + } + + // Count successes and failures + successCount := 0 + failureCount := 0 + + for _, result := range results { + if result.Success { + successCount++ + } else { + failureCount++ + // Log first few errors for debugging + if failureCount <= 5 { + log.Printf("Import error: %s", result.Error) + } + } + } + + log.Printf("Bulk import completed: %d succeeded, %d failed", successCount, failureCount) + + if failureCount > 0 && failureCount > lineCount/2 { + // Only error if more than half failed + return fmt.Errorf("bulk import had too many failures: %d out of %d", failureCount, lineCount) + } + + return nil +} + +// CheckCollectionDocumentCount returns the number of documents in a collection +func CheckCollectionDocumentCount(ctx context.Context, collectionName string) (int, error) { + collection, err := Client.Collection(collectionName).Retrieve(ctx) + if err != nil { + return 0, fmt.Errorf("failed to retrieve collection: %w", err) + } + + return int(*collection.NumDocuments), nil +} + +// InitializeDataIfEmpty checks if collection is empty and imports data if needed +// This is idempotent - safe to run on every startup +func InitializeDataIfEmpty(ctx context.Context, collectionName, dataFilePath string) error { + log.Printf("Checking if collection '%s' needs data initialization...", collectionName) + + // Check current document count + count, err := CheckCollectionDocumentCount(ctx, collectionName) + if err != nil { + return fmt.Errorf("failed to check document count: %w", err) + } + + if count > 0 { + log.Printf("Collection '%s' already has %d documents, skipping import", collectionName, count) + return nil + } + + log.Printf("Collection '%s' is empty, importing data from %s", collectionName, dataFilePath) + + // Import data + if err := ImportDocumentsFromJSONL(ctx, collectionName, dataFilePath); err != nil { + return fmt.Errorf("failed to import data: %w", err) + } + + // Verify import + newCount, err := CheckCollectionDocumentCount(ctx, collectionName) + if err != nil { + return fmt.Errorf("failed to verify import: %w", err) + } + + log.Printf("Data import successful: collection '%s' now has %d documents", collectionName, newCount) + return nil +} diff --git a/typesense-gin-full-text-search/utils/env.go b/typesense-gin-full-text-search/utils/env.go new file mode 100644 index 0000000..0fa36d5 --- /dev/null +++ b/typesense-gin-full-text-search/utils/env.go @@ -0,0 +1,44 @@ +package utils + +import ( + "log" + "os" + "strconv" + + "github.com/joho/godotenv" +) + +// init() function runs automatically when the package is imported +func init() { + // Load .env file + if err := godotenv.Load(); err != nil { + log.Println("No .env file found, using environment variables") + } +} + +// Helper functions to read environment variables with defaults +func GetEnv(key, defaultValue string) string { + if value := os.Getenv(key); value != "" { + return value + } + return defaultValue +} + +func GetEnvAsInt(key string, defaultValue int) int { + if value := os.Getenv(key); value != "" { + if intValue, err := strconv.Atoi(value); err == nil { + return intValue + } + } + return defaultValue +} + +func GetServerURL() string { + protocol := GetEnv("TYPESENSE_PROTOCOL", "http") + host := GetEnv("TYPESENSE_HOST", "localhost") + port := GetEnvAsInt("TYPESENSE_PORT", 8108) + return protocol + "://" + host + ":" + strconv.Itoa(port) +} + +// Collection name for books +var BookCollection = GetEnv("TYPESENSE_COLLECTION", "books") diff --git a/typesense-gin-full-text-search/utils/typesense.go b/typesense-gin-full-text-search/utils/typesense.go new file mode 100644 index 0000000..2650eee --- /dev/null +++ b/typesense-gin-full-text-search/utils/typesense.go @@ -0,0 +1,22 @@ +package utils + +import ( + "log" + + "github.com/typesense/typesense-go/v4/typesense" +) + +var Client *typesense.Client + +func init() { + apiKey := GetEnv("TYPESENSE_API_KEY", "xyz") + serverURL := GetServerURL() + + // Create client with simple configuration + Client = typesense.NewClient( + typesense.WithServer(serverURL), + typesense.WithAPIKey(apiKey), + ) + + log.Printf("Typesense Client created successfully") +} diff --git a/typesense-react-native-search-bar/App.tsx b/typesense-react-native-search-bar/App.tsx index 4805609..ecc76e3 100644 --- a/typesense-react-native-search-bar/App.tsx +++ b/typesense-react-native-search-bar/App.tsx @@ -1,49 +1,25 @@ import { StatusBar } from "expo-status-bar"; -import { useEffect, useState } from "react"; -import { StyleSheet, View, ScrollView, TextInput } from "react-native"; +import { StyleSheet, View, ScrollView } from "react-native"; import { SafeAreaProvider, useSafeAreaInsets, } from "react-native-safe-area-context"; +import { InstantSearch } from "react-instantsearch-core"; import { Heading } from "./components/Heading"; import { BookList } from "./components/BookList"; -import { Document } from "./types/Book"; -import { search } from "./utils/typesense"; import { SearchInput } from "./components/SearchInput"; +import { searchClient } from "./utils/typesense"; function AppContent() { - const [books, setBooks] = useState([]); - const [searchQuery, setSearchQuery] = useState(""); const insets = useSafeAreaInsets(); - useEffect(() => { - fetchBooks().catch((error) => { - console.error("Error fetching books:", error); - }); - }, []); - - useEffect(() => { - fetchBooks().catch((error) => { - console.error("Error fetching books:", error); - }); - }, [searchQuery]); - - async function fetchBooks() { - const data = await search(searchQuery); - setBooks(data); - } - return ( - + - + @@ -54,7 +30,9 @@ function AppContent() { export default function App() { return ( - + + + ); } @@ -64,20 +42,6 @@ const styles = StyleSheet.create({ flex: 1, backgroundColor: "#222", }, - headerContainer: { - paddingTop: 20, - paddingRight: 20, - paddingLeft: 20, - paddingBottom: 10, - borderRadius: 15, - marginHorizontal: 20, - marginBottom: 20, - shadowColor: "#000", - shadowOffset: { width: 0, height: 4 }, - shadowOpacity: 0.3, - shadowRadius: 8, - elevation: 8, - }, grid: { flexDirection: "row", flexWrap: "wrap", diff --git a/typesense-react-native-search-bar/README.md b/typesense-react-native-search-bar/README.md index bd96897..cbb3d0e 100644 --- a/typesense-react-native-search-bar/README.md +++ b/typesense-react-native-search-bar/README.md @@ -6,6 +6,8 @@ A modern search bar application built with React Native (Expo) and Typesense, fe - React Native (Expo) - Typesense +- react-instantsearch-core +- typesense-instantsearch-adapter - TypeScript ## Prerequisites diff --git a/typesense-react-native-search-bar/components/BookList.tsx b/typesense-react-native-search-bar/components/BookList.tsx index 3aee15e..788d007 100644 --- a/typesense-react-native-search-bar/components/BookList.tsx +++ b/typesense-react-native-search-bar/components/BookList.tsx @@ -1,12 +1,15 @@ import React from "react"; -import { Document } from "../types/Book"; +import { useHits } from "react-instantsearch-core"; import { BookCard } from "./BookCard"; +import { Book } from "../types/Book"; + +export const BookList = () => { + const { items } = useHits(); -export const BookList = ({ books }: { books: Document[] }) => { return ( <> - {books.map((book) => ( - + {items.map((book) => ( + ))} ); diff --git a/typesense-react-native-search-bar/components/SearchInput.tsx b/typesense-react-native-search-bar/components/SearchInput.tsx index bb14347..880e88e 100644 --- a/typesense-react-native-search-bar/components/SearchInput.tsx +++ b/typesense-react-native-search-bar/components/SearchInput.tsx @@ -1,24 +1,17 @@ import React from "react"; import { StyleSheet, TextInput } from "react-native"; +import { useSearchBox } from "react-instantsearch-core"; -interface SearchInputProps { - value: string; - onChangeText: (text: string) => void; - placeholder?: string; -} +export const SearchInput = () => { + const { query, refine } = useSearchBox(); -export const SearchInput = ({ - value, - onChangeText, - placeholder, -}: SearchInputProps) => { return ( ); }; diff --git a/typesense-react-native-search-bar/package.json b/typesense-react-native-search-bar/package.json index 8cfa326..d96caeb 100644 --- a/typesense-react-native-search-bar/package.json +++ b/typesense-react-native-search-bar/package.json @@ -12,9 +12,11 @@ "expo": "~54.0.32", "expo-status-bar": "~3.0.9", "react": "19.1.0", + "react-instantsearch-core": "^7.23.2", "react-native": "0.81.5", "react-native-safe-area-context": "^5.6.2", - "react-native-svg": "^15.15.1" + "react-native-svg": "^15.15.1", + "typesense-instantsearch-adapter": "^2.9.0" }, "devDependencies": { "@types/react": "~19.1.0", diff --git a/typesense-react-native-search-bar/types/Book.ts b/typesense-react-native-search-bar/types/Book.ts index d0a0245..d7a3a8b 100644 --- a/typesense-react-native-search-bar/types/Book.ts +++ b/typesense-react-native-search-bar/types/Book.ts @@ -4,8 +4,6 @@ export interface Book { authors: string[]; image_url: string; publication_year: number; -} - -export interface Document { - document: Book; + average_rating?: number; + ratings_count?: number; } diff --git a/typesense-react-native-search-bar/utils/typesense.ts b/typesense-react-native-search-bar/utils/typesense.ts index eedb51e..7ef6e79 100644 --- a/typesense-react-native-search-bar/utils/typesense.ts +++ b/typesense-react-native-search-bar/utils/typesense.ts @@ -1,22 +1,19 @@ -import { Document } from "../types/Book"; +import TypesenseInstantSearchAdapter from "typesense-instantsearch-adapter"; -export const search = async (searchQuery: string): Promise => { - const url = `${process.env.EXPO_PUBLIC_TYPESENSE_PROTOCOL}://${process.env.EXPO_PUBLIC_TYPESENSE_HOST}:${process.env.EXPO_PUBLIC_TYPESENSE_PORT}/collections/books/documents/search?q=${encodeURIComponent( - searchQuery, - )}&query_by=title,authors`; +const typesenseInstantsearchAdapter = new TypesenseInstantSearchAdapter({ + server: { + apiKey: process.env.EXPO_PUBLIC_TYPESENSE_API_KEY || "xyz", + nodes: [ + { + host: process.env.EXPO_PUBLIC_TYPESENSE_HOST || "localhost", + port: Number(process.env.EXPO_PUBLIC_TYPESENSE_PORT) || 8108, + protocol: process.env.EXPO_PUBLIC_TYPESENSE_PROTOCOL || "http", + }, + ], + }, + additionalSearchParameters: { + query_by: "title,authors", + }, +}); - const response = await fetch(url, { - method: "GET", - headers: { - "X-TYPESENSE-API-KEY": process.env.EXPO_PUBLIC_TYPESENSE_API_KEY || "xyz", - "Content-Type": "application/json", - }, - }); - - if (!response.ok) { - throw new Error("Typesense search failed"); - } - - const data = await response.json(); - return data?.hits || []; -}; +export const searchClient = typesenseInstantsearchAdapter.searchClient;