Initial commit
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
/dist
|
||||||
|
.envrc
|
||||||
45
README.md
Normal file
45
README.md
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
# SonicClient
|
||||||
|
This is a rudimentary (Sonic)[https://github.com/valeriansaliou/sonic]
|
||||||
|
command-line client that I'm using to interact with a locally running
|
||||||
|
service.
|
||||||
|
|
||||||
|
## Server
|
||||||
|
You'll need a server running, to spin one up you can
|
||||||
|
use the `docker-compose.yml` file in the `demo`
|
||||||
|
directory, which pulls down the docker image from
|
||||||
|
the docker hub.
|
||||||
|
|
||||||
|
The required directory structure (`data/kv` and `data/fst`)
|
||||||
|
is already in place, so to spin it up you only need to:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ docker-compose up -d; docker-compose logs -f
|
||||||
|
```
|
||||||
|
|
||||||
|
## Client
|
||||||
|
To build the client you need a (Nim)[https://nim-lang.org/] compiler
|
||||||
|
for your target architecture; once it's available, along with the
|
||||||
|
`nimble` tool, you can build the client with:
|
||||||
|
```sh
|
||||||
|
$ nimble build --verbose -d:release
|
||||||
|
```
|
||||||
|
and the binary will be left in the `./dist` directory.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
Once the client is build, run:
|
||||||
|
```
|
||||||
|
$ ./dist/sc --help
|
||||||
|
```
|
||||||
|
to display the commands and options required.
|
||||||
|
|
||||||
|
## Environment variables
|
||||||
|
The file `_envrc` contains an `.envrc` template for your
|
||||||
|
convenience; the client needs these three environment variables
|
||||||
|
set up so it knows what server to interact with:
|
||||||
|
|
||||||
|
* `SONIC_HOST`
|
||||||
|
* `SONIC_PORT`
|
||||||
|
* `SONIC_SECRET`
|
||||||
|
|
||||||
|
You may use any method to set them up, and the `.envrc` method
|
||||||
|
is just a convenience for those that use `direnv`.
|
||||||
4
_envrc
Normal file
4
_envrc
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
export SONIC_HOST=127.0.0.1 # or some other ip address or hostname
|
||||||
|
export SONIC_PORT=1491 # or some other port?
|
||||||
|
export SONIC_SECRET="PthRtZ0m2MFLG" # or whatever password you set up
|
||||||
1
demo/data/fst/.delete.me
Normal file
1
demo/data/fst/.delete.me
Normal file
@@ -0,0 +1 @@
|
|||||||
|
index data directory
|
||||||
1
demo/data/kv/.delete.me
Normal file
1
demo/data/kv/.delete.me
Normal file
@@ -0,0 +1 @@
|
|||||||
|
index data directory
|
||||||
18
demo/docker-compose.yml
Normal file
18
demo/docker-compose.yml
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
---
|
||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
sonic:
|
||||||
|
image: valeriansaliou/sonic:v1.4.0
|
||||||
|
volumes:
|
||||||
|
- type: bind
|
||||||
|
source: ./sonic.cfg
|
||||||
|
target: /etc/sonic.cfg
|
||||||
|
- type: bind
|
||||||
|
source: ./data
|
||||||
|
target: /data
|
||||||
|
ports:
|
||||||
|
- 1491:1491
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
# vim: ai:et:ts=2:sw=2:wm=0:
|
||||||
51
demo/sonic.cfg
Normal file
51
demo/sonic.cfg
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
# Sonic
|
||||||
|
# Fast, lightweight and schema-less search backend
|
||||||
|
# Configuration file
|
||||||
|
# Example: https://github.com/valeriansaliou/sonic/blob/master/config.cfg
|
||||||
|
|
||||||
|
[server]
|
||||||
|
log_level = "info"
|
||||||
|
|
||||||
|
[channel]
|
||||||
|
inet = "0.0.0.0:1491"
|
||||||
|
tcp_timeout = 300
|
||||||
|
auth_password = "PthRtZ0m2MFLG"
|
||||||
|
|
||||||
|
[channel.search]
|
||||||
|
query_limit_default = 10
|
||||||
|
query_limit_maximum = 100
|
||||||
|
query_alternates_try = 4
|
||||||
|
suggest_limit_default = 5
|
||||||
|
suggest_limit_maximum = 20
|
||||||
|
list_limit_default = 100
|
||||||
|
list_limit_maximum = 500
|
||||||
|
|
||||||
|
[store]
|
||||||
|
|
||||||
|
[store.kv]
|
||||||
|
path = "/data/kv/"
|
||||||
|
retain_word_objects = 1000
|
||||||
|
|
||||||
|
[store.kv.pool]
|
||||||
|
inactive_after = 1800
|
||||||
|
|
||||||
|
[store.kv.database]
|
||||||
|
flush_after = 900
|
||||||
|
compress = true
|
||||||
|
parallelism = 2
|
||||||
|
max_files = 100
|
||||||
|
max_compactions = 1
|
||||||
|
max_flushes = 1
|
||||||
|
write_buffer = 16384
|
||||||
|
write_ahead_log = true
|
||||||
|
|
||||||
|
[store.fst]
|
||||||
|
path = "/data/fst/"
|
||||||
|
|
||||||
|
[store.fst.pool]
|
||||||
|
inactive_after = 300
|
||||||
|
|
||||||
|
[store.fst.graph]
|
||||||
|
consolidate_after = 180
|
||||||
|
max_size = 2048
|
||||||
|
max_words = 250000
|
||||||
12
sc.nimble
Normal file
12
sc.nimble
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# Package
|
||||||
|
version = "0.1.0"
|
||||||
|
author = "Gustavo Cordova Avila"
|
||||||
|
description = "Sonic search infra client"
|
||||||
|
license = "Apache-2.0"
|
||||||
|
srcDir = "src"
|
||||||
|
binDir = "dist"
|
||||||
|
bin = @["sc"]
|
||||||
|
|
||||||
|
# Dependencies
|
||||||
|
requires "nim >= 1.6.8"
|
||||||
|
requires "sonic >= 0.1.0"
|
||||||
0
src/nim.cfg
Normal file
0
src/nim.cfg
Normal file
245
src/sc.nim
Normal file
245
src/sc.nim
Normal file
@@ -0,0 +1,245 @@
|
|||||||
|
## Ingest a file's content into Sonic
|
||||||
|
import os
|
||||||
|
import strutils
|
||||||
|
import sonic
|
||||||
|
|
||||||
|
var
|
||||||
|
verbose: bool = false
|
||||||
|
|
||||||
|
const
|
||||||
|
USAGE_DOC = staticRead("../static/usage.txt")
|
||||||
|
|
||||||
|
proc showUsage(msg = "") =
|
||||||
|
## Display a usage message and quit
|
||||||
|
if msg != "":
|
||||||
|
stderr.writeLine "ERROR: $#\n" % msg
|
||||||
|
quit USAGE_DOC.replace("${app}", getAppFilename()),
|
||||||
|
(if msg == "": 0 else: 10)
|
||||||
|
|
||||||
|
proc envParam(name: string; default = ""): string =
|
||||||
|
## Return an environment parameter or return an error
|
||||||
|
let eVal = getEnv(name, default)
|
||||||
|
if eVal == "":
|
||||||
|
quit("Expected a value: $" & name, 1)
|
||||||
|
return eVal
|
||||||
|
|
||||||
|
proc getChannel(mode = SonicChannel.Ingest): Sonic =
|
||||||
|
## Return a channel based on command line params
|
||||||
|
let
|
||||||
|
host = envParam("SONIC_HOST")
|
||||||
|
port = envParam("SONIC_PORT")
|
||||||
|
secret = envParam("SONIC_SECRET")
|
||||||
|
try:
|
||||||
|
return open(host, port.parseInt(), secret, mode)
|
||||||
|
except ValueError:
|
||||||
|
let err = getCurrentExceptionMsg()
|
||||||
|
quit "Not a valid number: $#\n$#" % [port, err], 1
|
||||||
|
|
||||||
|
proc consolidate(channel: Sonic) =
|
||||||
|
## Trigger a consolidation
|
||||||
|
discard channel.execCommand("TRIGGER", @["consolidate"])
|
||||||
|
|
||||||
|
proc close(chn: Sonic) =
|
||||||
|
## Close a sonic channel
|
||||||
|
let outp = chn.quit()
|
||||||
|
if verbose:
|
||||||
|
stderr.writeLine "<closing $# channel: #$>" % [$(chn.channel), outp]
|
||||||
|
|
||||||
|
proc intAt(args: openArray[string]; pos: int; default: int): int =
|
||||||
|
## Parse a positional parameter if it exists, if not use default
|
||||||
|
if args.len-1 < pos or args[pos] == "":
|
||||||
|
return default
|
||||||
|
try:
|
||||||
|
return args[pos].parseInt()
|
||||||
|
except:
|
||||||
|
let err = getCurrentExceptionMsg()
|
||||||
|
quit("Not a valid number: $#\n$#" % [args[pos], err], 1)
|
||||||
|
|
||||||
|
################################################################
|
||||||
|
## Execute the user-facing commands
|
||||||
|
##
|
||||||
|
proc cmdPing() =
|
||||||
|
## Execute the "ping" command
|
||||||
|
let
|
||||||
|
chn = getChannel(SonicChannel.Control)
|
||||||
|
response = chn.execCommand("PING")
|
||||||
|
chn.close()
|
||||||
|
quit response, if response == "PONG": 0 else: 1
|
||||||
|
|
||||||
|
proc cmdCount(collection, bucket, objId: string) =
|
||||||
|
## Return indexed search data count for collection/bucket/objId
|
||||||
|
let
|
||||||
|
chn = getChannel(SonicChannel.Search)
|
||||||
|
response = chn.count(collection, bucket, objId)
|
||||||
|
chn.close()
|
||||||
|
quit $response, 0
|
||||||
|
|
||||||
|
proc cmdPush(collection, bucket, obj, data: string) =
|
||||||
|
## Ingest a file's content into a Sonic instance
|
||||||
|
var
|
||||||
|
justOne = false
|
||||||
|
stream = stdin
|
||||||
|
|
||||||
|
if data.len == 0:
|
||||||
|
quit "Data is an empty string", 1
|
||||||
|
elif data == "-":
|
||||||
|
stderr.writeLine "Reading from <stdin>"
|
||||||
|
elif data[0] == '@':
|
||||||
|
let filename = data[1 ..< data.len]
|
||||||
|
try:
|
||||||
|
stream = open(filename, bufSize=8000)
|
||||||
|
except:
|
||||||
|
let err = getCurrentExceptionMsg()
|
||||||
|
quit "Can't open \"$#\":\n$#" % [filename, err], 10
|
||||||
|
else:
|
||||||
|
justOne = true
|
||||||
|
|
||||||
|
var
|
||||||
|
ingCh = getChannel(SonicChannel.Ingest)
|
||||||
|
ctlCh = getChannel(SonicChannel.Control)
|
||||||
|
rMsg = ""
|
||||||
|
rCode = 0
|
||||||
|
|
||||||
|
if justOne:
|
||||||
|
let pushedOk = ingCh.push(collection, bucket, obj, data)
|
||||||
|
ctlCh.consolidate()
|
||||||
|
rMsg = if pushedOk: "" else: "push command returned a warning"
|
||||||
|
rCode = if pushedOk: 0 else: 1
|
||||||
|
else:
|
||||||
|
let
|
||||||
|
objIdPrefix = if data[0] == '@': "$#/$#:" % [obj, data[1 ..< data.len]]
|
||||||
|
else: obj & ":"
|
||||||
|
var
|
||||||
|
line = newStringOfCap(256)
|
||||||
|
count = 0
|
||||||
|
stderr.write("push: ")
|
||||||
|
while stream.readLine(line):
|
||||||
|
let
|
||||||
|
objId = objIdPrefix & $count
|
||||||
|
pushed = ingCh.push(collection, bucket, objId, line)
|
||||||
|
stderr.write(if pushed: "." else: "x")
|
||||||
|
inc count
|
||||||
|
if (count mod 31) == 0:
|
||||||
|
ctlCh.consolidate()
|
||||||
|
stderr.write("#")
|
||||||
|
stderr.write("\n")
|
||||||
|
if data[0] == '@':
|
||||||
|
close(stream)
|
||||||
|
ctlCh.close()
|
||||||
|
ingCh.close()
|
||||||
|
quit rMsg, rCode
|
||||||
|
|
||||||
|
proc cmdPop(collection, bucket, obj, data: string) =
|
||||||
|
## Pop search data from the given collection/bucket/obj
|
||||||
|
if data.len > 0:
|
||||||
|
let
|
||||||
|
ingCh = getChannel(SonicChannel.Ingest)
|
||||||
|
ctlCh = getChannel(SonicChannel.Control)
|
||||||
|
popOut = ingCh.pop(collection, bucket, obj, data)
|
||||||
|
ctlCh.consolidate()
|
||||||
|
ctlCh.close()
|
||||||
|
ingCh.close()
|
||||||
|
quit $popOut, 0
|
||||||
|
quit "Data is an empty string", 1
|
||||||
|
|
||||||
|
proc cmdQuery(collection, bucket, terms: string; limit, offset: int) =
|
||||||
|
## Query the indexes, echo the results to stdout
|
||||||
|
let
|
||||||
|
srChn = getChannel(SonicChannel.Search)
|
||||||
|
results = srChn.query(collection, bucket, terms, limit, offset)
|
||||||
|
srChn.close()
|
||||||
|
quit(results.join("\n"), 0)
|
||||||
|
|
||||||
|
proc cmdSuggest(collection, bucket, word: string; limit: int) =
|
||||||
|
## Query suggestions based on the word
|
||||||
|
let
|
||||||
|
srChn = getChannel(SonicChannel.Search)
|
||||||
|
results = srChn.suggest(collection, bucket, word, limit)
|
||||||
|
srChn.close()
|
||||||
|
quit(results.join("\n"), 0)
|
||||||
|
|
||||||
|
proc cmdFlush(collection: string; bucket=""; objId="") =
|
||||||
|
## Flushes all indexed data for the given collection, bucket or object
|
||||||
|
let
|
||||||
|
cnChn = getChannel(SonicChannel.Control)
|
||||||
|
results = cnChn.flush(collection, bucket, objId)
|
||||||
|
cnChn.close()
|
||||||
|
quit($results, 0)
|
||||||
|
|
||||||
|
################################################################
|
||||||
|
## Parse the command line and dispatch appropriate actions
|
||||||
|
##
|
||||||
|
proc main() =
|
||||||
|
## Parse the command line, dispatch the appropriate actions
|
||||||
|
var args = commandLineParams()
|
||||||
|
|
||||||
|
if args.len == 0 or "-h" in args or "--help" in args:
|
||||||
|
showUsage()
|
||||||
|
|
||||||
|
verbose = ("-v" in args) or ("--verbose" in args)
|
||||||
|
if verbose:
|
||||||
|
while "-v" in args:
|
||||||
|
args.delete(args.find("-v"))
|
||||||
|
while "--verbose" in args:
|
||||||
|
args.delete(args.find("--verbose"))
|
||||||
|
|
||||||
|
case args[0]:
|
||||||
|
of "help":
|
||||||
|
showUsage()
|
||||||
|
|
||||||
|
of "ping": # ping
|
||||||
|
if args.len > 1:
|
||||||
|
showUsage("'ping' takes no arguments")
|
||||||
|
cmdPing()
|
||||||
|
|
||||||
|
of "count": # count <collection> [bucket [object]]
|
||||||
|
let
|
||||||
|
bucket = (if args.len >= 3: args[2] else: "")
|
||||||
|
objId = (if args.len >= 4: args[3] else: "")
|
||||||
|
if args.len > 4:
|
||||||
|
showUsage("Too many arguments for 'count'")
|
||||||
|
cmdCount(args[1], bucket, objId)
|
||||||
|
|
||||||
|
of "push": # push <collection> <bucket> <object> "data|@filename|-"
|
||||||
|
if args.len != 5:
|
||||||
|
let pre = if args.len < 5: "Missing" else: "Too many"
|
||||||
|
showUsage(pre & " arguments for 'push'")
|
||||||
|
cmdPush(args[1], args[2], args[3], args[4])
|
||||||
|
|
||||||
|
of "pop": # pop <collection> <bucket> <object> "data"
|
||||||
|
if args.len != 5:
|
||||||
|
let pre = if args.len < 5: "Missing" else: "Too many"
|
||||||
|
showUsage(pre & " arguments for 'pop'")
|
||||||
|
cmdPop(args[1], args[2], args[3], args[4])
|
||||||
|
|
||||||
|
of "query": # query <collection> <bucket> "terms" [limit=10] [offset=0]
|
||||||
|
if args.len < 4 or args.len > 6:
|
||||||
|
let pre = if args.len < 4: "Missing" else: "Too many"
|
||||||
|
showUsage(pre & " arguments for 'query'")
|
||||||
|
let
|
||||||
|
limit = args.intAt(4, 10)
|
||||||
|
offset = args.intAt(5, 0)
|
||||||
|
cmdQuery(args[1], args[2], args[3], limit, offset)
|
||||||
|
|
||||||
|
of "suggest": # query <collection> <bucket> "word" [limit=10]
|
||||||
|
if args.len < 4 or args.len > 5:
|
||||||
|
let pre = if args.len < 4: "Missing" else: "Too many"
|
||||||
|
showUsage(pre & " arguments for 'suggest'")
|
||||||
|
let
|
||||||
|
limit = args.intAt(4, 10)
|
||||||
|
cmdSuggest(args[1], args[2], args[3], limit)
|
||||||
|
|
||||||
|
of "flush": # flush <collection> [bucket [object]]
|
||||||
|
if args.len < 3 or args.len > 5:
|
||||||
|
let pre = if args.len < 3: "Missing" else: "Too many"
|
||||||
|
showUsage(pre & " arguments for 'flush'")
|
||||||
|
let
|
||||||
|
bucket = (if args.len >= 3: args[3] else: "")
|
||||||
|
objId = (if args.len >= 4: args[4] else: "")
|
||||||
|
cmdFlush(args[2], bucket, objId)
|
||||||
|
|
||||||
|
else:
|
||||||
|
showUsage("Unknown command: " & args[0])
|
||||||
|
|
||||||
|
main()
|
||||||
|
# Fin
|
||||||
66
static/usage.txt
Normal file
66
static/usage.txt
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
Usage:
|
||||||
|
======
|
||||||
|
${app} ping
|
||||||
|
${app} push <collection> <bucket> <object> "data|@filename|-"
|
||||||
|
${app} pop <collection> <bucket> <object> "search data to pop"
|
||||||
|
${app} query <collection> <bucket> "search terms" [limit] [offset]
|
||||||
|
${app} suggest <collection> <bucket> "word" [limit]
|
||||||
|
${app} flush <collection> [ bucket [object] ]
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
=========
|
||||||
|
ping - Verify server is connected, returns "PONG"
|
||||||
|
|
||||||
|
count - Return the count of indexed data, requires:
|
||||||
|
+ collection
|
||||||
|
+ bucket; optional
|
||||||
|
+ object; optional
|
||||||
|
|
||||||
|
push - Ingest search data into the server, requires:
|
||||||
|
+ collection
|
||||||
|
+ bucket
|
||||||
|
+ object
|
||||||
|
+ "quoted text on the command line"
|
||||||
|
-or-
|
||||||
|
"@filename.ext" where to read the text
|
||||||
|
-or-
|
||||||
|
"-" to read from stdin
|
||||||
|
|
||||||
|
pop - Pop data from the search indexes, requires:
|
||||||
|
+ collection
|
||||||
|
+ bucket
|
||||||
|
+ object
|
||||||
|
+ "quoted text on the command line"
|
||||||
|
|
||||||
|
query - Query the indexes for information, requires:
|
||||||
|
+ collection
|
||||||
|
+ bucket
|
||||||
|
+ "search terms"
|
||||||
|
+ limit, optional; default: 10
|
||||||
|
+ offset, optional; default: 0
|
||||||
|
|
||||||
|
Outputs the object IDs that match the given search terms
|
||||||
|
|
||||||
|
suggest - Request suggestions for a given word, requires:
|
||||||
|
+ collection
|
||||||
|
+ bucket
|
||||||
|
+ "word"
|
||||||
|
+ limit, optional; default: 10
|
||||||
|
|
||||||
|
Outputs the suggestions to stdout
|
||||||
|
|
||||||
|
flush - Flush all index data for the given collection, bucket or object
|
||||||
|
+ collection
|
||||||
|
+ bucket; optional
|
||||||
|
+ object; optional
|
||||||
|
|
||||||
|
|
||||||
|
Environment variables:
|
||||||
|
======================
|
||||||
|
These variables point to the Sonic service the application
|
||||||
|
will be connecting to:
|
||||||
|
|
||||||
|
+ SONIC_HOST: Hostname or ip address of the service
|
||||||
|
+ SONIC_PORT: Port that the service is listening on
|
||||||
|
+ SONIC_SECRET: Password required to connect to sonic
|
||||||
|
|
||||||
Reference in New Issue
Block a user