├── .gitignore ├── example ├── view │ ├── static │ │ └── .gitkeep │ ├── src │ │ ├── router │ │ │ ├── _import_production.js │ │ │ ├── _import_development.js │ │ │ └── index.js │ │ ├── assets │ │ │ └── logo.png │ │ ├── api │ │ │ └── example.js │ │ ├── App.vue │ │ ├── main.js │ │ ├── utils │ │ │ └── fetch.js │ │ └── views │ │ │ └── index.vue │ ├── config │ │ ├── prod.env.js │ │ ├── dev.env.js │ │ └── index.js │ ├── .editorconfig │ ├── .gitignore │ ├── .postcssrc.js │ ├── build │ │ ├── dev-client.js │ │ ├── vue-loader.conf.js │ │ ├── build.js │ │ ├── webpack.dev.conf.js │ │ ├── check-versions.js │ │ ├── webpack.base.conf.js │ │ ├── utils.js │ │ ├── dev-server.js │ │ └── webpack.prod.conf.js │ ├── .babelrc │ ├── index.html │ ├── README.md │ └── package.json ├── .gitignore └── main.go ├── lbsengine.pdf ├── types ├── search_response.go ├── scored_document.go ├── indexed_document.go ├── search_request.go ├── options.go ├── indexed_document_gen_test.go └── indexed_document_gen.go ├── dbtest ├── cachego_test.go ├── gocache_test.go ├── redis_test.go ├── tidb_test.go └── Earthpoint_test.go ├── spider ├── spider_test.go ├── jsonOperate.go └── spider.go ├── core ├── cacher.go ├── geohash_test.go ├── geohash.go ├── indexer_test.go └── indexer.go ├── Makefile ├── README.md ├── LICENSE ├── engine ├── worker.go ├── engine_test.go └── engine.go └── distanceMeasure ├── distanceMeasure_test.go └── distanceMeasure.go /.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | -------------------------------------------------------------------------------- /example/view/static/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | example 2 | -------------------------------------------------------------------------------- /lbsengine.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sillydong/lbsengine/HEAD/lbsengine.pdf -------------------------------------------------------------------------------- /example/view/src/router/_import_production.js: -------------------------------------------------------------------------------- 1 | module.exports = file => () => import('@/views/' + file + '.vue') 2 | -------------------------------------------------------------------------------- /example/view/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sillydong/lbsengine/HEAD/example/view/src/assets/logo.png -------------------------------------------------------------------------------- /example/view/config/prod.env.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | module.exports = { 3 | NODE_ENV: '"production"', 4 | BASE_API: '"/"' 5 | } 6 | -------------------------------------------------------------------------------- /example/view/src/router/_import_development.js: -------------------------------------------------------------------------------- 1 | module.exports = file => require('@/views/' + file + '.vue').default // vue-loader at least v13.0.0+ 2 | -------------------------------------------------------------------------------- /types/search_response.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | //搜索结果返回 4 | type SearchResponse struct { 5 | Docs ScoredDocuments //排序好的结果 6 | Timeout bool //是否超时 7 | Count int //数量 8 | } 9 | -------------------------------------------------------------------------------- /example/view/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /example/view/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | dist/ 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Editor directories and files 9 | .idea 10 | .vscode 11 | *.suo 12 | *.ntvs* 13 | *.njsproj 14 | *.sln 15 | -------------------------------------------------------------------------------- /example/view/.postcssrc.js: -------------------------------------------------------------------------------- 1 | // https://github.com/michael-ciniawsky/postcss-load-config 2 | 3 | module.exports = { 4 | "plugins": { 5 | // to edit target browsers: use "browserslist" field in package.json 6 | "autoprefixer": {} 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /example/view/config/dev.env.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const merge = require('webpack-merge') 3 | const prodEnv = require('./prod.env') 4 | 5 | module.exports = merge(prodEnv, { 6 | NODE_ENV: '"development"', 7 | BASE_API: '"http://localhost:8081"' 8 | }) 9 | -------------------------------------------------------------------------------- /example/view/build/dev-client.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 'use strict' 3 | require('eventsource-polyfill') 4 | var hotClient = require('webpack-hot-middleware/client?noInfo=true&reload=true') 5 | 6 | hotClient.subscribe(function (event) { 7 | if (event.action === 'reload') { 8 | window.location.reload() 9 | } 10 | }) 11 | -------------------------------------------------------------------------------- /example/view/src/api/example.js: -------------------------------------------------------------------------------- 1 | import {get,post,del} from "@/utils/fetch"; 2 | 3 | export function api_add(form){ 4 | return post('/api/add',form) 5 | } 6 | 7 | export function api_del(id){ 8 | return del('/api/del/'+id) 9 | } 10 | 11 | export function api_query(params){ 12 | return get("/api/query",params) 13 | } 14 | 15 | -------------------------------------------------------------------------------- /example/view/src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | 4 | const _import = require('./_import_' + process.env.NODE_ENV) 5 | 6 | Vue.use(Router) 7 | 8 | export default new Router({ 9 | routes: [ 10 | { 11 | path: '/', 12 | name: 'LBS Example', 13 | component: _import('index') 14 | } 15 | ] 16 | }) 17 | -------------------------------------------------------------------------------- /example/view/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "modules": false, 5 | "targets": { 6 | "browsers": ["> 1%", "last 2 versions", "not ie <= 8"] 7 | } 8 | }], 9 | "stage-2" 10 | ], 11 | "plugins": ["transform-runtime"], 12 | "env": { 13 | "test": { 14 | "presets": ["env", "stage-2",["es2015", { "modules": false }]], 15 | "plugins": ["istanbul"] 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /example/view/src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | 13 | 24 | -------------------------------------------------------------------------------- /example/view/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | lbsexample 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /types/scored_document.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | //排序的struct 4 | type ScoredDocument struct { 5 | Distance float64 6 | DocId uint64 7 | Model interface{} //为外围组装数据提供 8 | } 9 | 10 | //排序 11 | type ScoredDocuments []*ScoredDocument 12 | 13 | func (docs ScoredDocuments) Len() int { 14 | return len(docs) 15 | } 16 | 17 | func (docs ScoredDocuments) Swap(i, j int) { 18 | docs[i], docs[j] = docs[j], docs[i] 19 | } 20 | 21 | func (docs ScoredDocuments) Less(i, j int) bool { 22 | return docs[i].Distance < docs[j].Distance 23 | } 24 | -------------------------------------------------------------------------------- /example/view/build/vue-loader.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const utils = require('./utils') 3 | const config = require('../config') 4 | const isProduction = process.env.NODE_ENV === 'production' 5 | 6 | module.exports = { 7 | loaders: utils.cssLoaders({ 8 | sourceMap: isProduction 9 | ? config.build.productionSourceMap 10 | : config.dev.cssSourceMap, 11 | extract: isProduction 12 | }), 13 | transformToRequire: { 14 | video: 'src', 15 | source: 'src', 16 | img: 'src', 17 | image: 'xlink:href' 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /types/indexed_document.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | //go:generate msgp 4 | 5 | //固态存储中存储的struct 6 | type IndexedDocument struct { 7 | DocId uint64 `msg:"id"` 8 | Latitude float64 `msg:"lat"` 9 | Longitude float64 `msg:"long"` 10 | Fields interface{} `msg:"f"` 11 | } 12 | 13 | //marshal 14 | func (z *IndexedDocument) MarshalBinary() (data []byte, err error) { 15 | return z.MarshalMsg(nil) 16 | } 17 | 18 | //unmarshal 19 | func (z *IndexedDocument) UnmarshalBinary(data []byte) error { 20 | _, err := z.UnmarshalMsg(data) 21 | return err 22 | } 23 | -------------------------------------------------------------------------------- /example/view/README.md: -------------------------------------------------------------------------------- 1 | # view 2 | 3 | > A Vue.js project 4 | 5 | ## Build Setup 6 | 7 | ``` bash 8 | # install dependencies 9 | npm install 10 | 11 | # serve with hot reload at localhost:8080 12 | npm run dev 13 | 14 | # build for production with minification 15 | npm run build 16 | 17 | # build for production and view the bundle analyzer report 18 | npm run build --report 19 | ``` 20 | 21 | For a detailed explanation on how things work, check out the [guide](http://vuejs-templates.github.io/webpack/) and [docs for vue-loader](http://vuejs.github.io/vue-loader). 22 | -------------------------------------------------------------------------------- /dbtest/cachego_test.go: -------------------------------------------------------------------------------- 1 | package dbtest 2 | 3 | import ( 4 | "fmt" 5 | "github.com/muesli/cache2go" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | var cachegoclient *cache2go.CacheTable 11 | 12 | func init() { 13 | cachegoclient = cache2go.Cache("mycache") 14 | } 15 | 16 | func BenchmarkBSet(b *testing.B) { 17 | for i := 0; i < b.N; i++ { 18 | key := fmt.Sprintf("key%v", i) 19 | value := fmt.Sprintf("value%v", i) 20 | cachegoclient.Add(key, 5*time.Minute, value) 21 | } 22 | } 23 | 24 | func BenchmarkBGet(b *testing.B) { 25 | for i := 0; i < b.N; i++ { 26 | key := fmt.Sprintf("key%v", i) 27 | cachegoclient.Value(key) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /dbtest/gocache_test.go: -------------------------------------------------------------------------------- 1 | package dbtest 2 | 3 | import ( 4 | "fmt" 5 | "github.com/patrickmn/go-cache" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | var gocacheclient *cache.Cache 11 | 12 | func init() { 13 | gocacheclient = cache.New(5*time.Minute, 10*time.Minute) 14 | } 15 | 16 | func BenchmarkASet(b *testing.B) { 17 | for i := 0; i < b.N; i++ { 18 | key := fmt.Sprintf("key%v", i) 19 | value := fmt.Sprintf("value%v", i) 20 | gocacheclient.Set(key, value, 5*time.Minute) 21 | } 22 | } 23 | 24 | func BenchmarkAGet(b *testing.B) { 25 | for i := 0; i < b.N; i++ { 26 | key := fmt.Sprintf("key%v", i) 27 | gocacheclient.Get(key) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /example/view/src/main.js: -------------------------------------------------------------------------------- 1 | // The Vue build version to load with the `import` command 2 | // (runtime-only or standalone) has been set in webpack.base.conf with an alias. 3 | import Vue from 'vue' 4 | import App from './App' 5 | import router from './router' 6 | 7 | import Mint from 'mint-ui' 8 | import 'mint-ui/lib/style.css' 9 | import VueAMap from 'vue-amap' 10 | 11 | Vue.use(Mint) 12 | Vue.use(VueAMap) 13 | VueAMap.initAMapApiLoader({ 14 | key: '8cc3327f86ecdbc7840b2bee9e0997da', 15 | plugin: ['Geolocation','Autocomplete','Scale','OverView','ToolBar'] 16 | }); 17 | 18 | Vue.config.productionTip = false 19 | 20 | /* eslint-disable no-new */ 21 | new Vue({ 22 | el: '#app', 23 | router, 24 | template: '', 25 | components: { App } 26 | }) 27 | -------------------------------------------------------------------------------- /spider/spider_test.go: -------------------------------------------------------------------------------- 1 | package spider 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/3zheng/railgun/PoolAndAgent" 8 | ) 9 | 10 | func TestGetGaodeData(t *testing.T) { 11 | //初始化并连接mysql数据库 12 | pDBProcess := PoolAndAgent.CreateADODatabase("root:123456@tcp(localhost:3306)/gotest?charset=utf8") 13 | pDBProcess.InitDB() 14 | 15 | for i := 1; i <= 40; i++ { 16 | str := fmt.Sprintf("%d", i) 17 | arrData := GetPOIData(str) 18 | fmt.Println("长度=", len(arrData), arrData) 19 | t.Log(arrData) 20 | 21 | for _, value := range arrData { 22 | sqlExpress := fmt.Sprintf("insert into poi_data(id, name, location, longitude, latitude) values( '%s', '%s', '%s', %f, %f)", value.ID, value.Name, value.Location, value.Coordinate.Longitude, value.Coordinate.Latitude) 23 | pDBProcess.WriteToDB(sqlExpress) 24 | } 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /types/search_request.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import "time" 4 | 5 | //搜索请求 6 | type SearchRequest struct { 7 | Latitude float64 8 | Longitude float64 9 | CountOnly bool 10 | Offset int 11 | Limit int 12 | SearchOption *SearchOptions //可留空,使用引擎默认参数 13 | } 14 | 15 | //可不设置的搜索参数 16 | type SearchOptions struct { 17 | Refresh bool 18 | OrderDesc bool 19 | Timeout time.Duration 20 | Accuracy int //计算进度 21 | Circles int //圈数,默认1,不扩散 22 | Excepts map[uint64]bool //排除指定ID 23 | Filter func(doc IndexedDocument) bool 24 | } 25 | 26 | func (o *SearchOptions) Init() { 27 | if o.Accuracy == 0 { 28 | o.Accuracy = STANDARD 29 | } 30 | if o.Circles == 0 { 31 | o.Circles = 1 32 | } 33 | } 34 | 35 | const ( 36 | _ = iota 37 | STANDARD //传统计算方法 38 | MEITUAN //美团开放计算方法 39 | IMPROVED //优化的计算方法 40 | ) 41 | -------------------------------------------------------------------------------- /core/cacher.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "github.com/patrickmn/go-cache" 5 | "github.com/sillydong/lbsengine/types" 6 | "time" 7 | ) 8 | 9 | //缓存器 10 | type Cacher struct { 11 | client *cache.Cache 12 | } 13 | 14 | func (c *Cacher) Init() { 15 | c.client = cache.New(5*time.Minute, 10*time.Minute) 16 | } 17 | 18 | //读缓存 19 | func (c *Cacher) Get(key string, offset, limit int) (types.ScoredDocuments, int) { 20 | if val, ok := c.client.Get(key); ok { 21 | docs := val.(types.ScoredDocuments) 22 | size := len(docs) 23 | if offset > size { 24 | return nil, size 25 | } else if offset+limit > size { 26 | return docs[offset:], size 27 | } else { 28 | return docs[offset : offset+limit], size 29 | } 30 | } 31 | return nil, 0 32 | } 33 | 34 | //写缓存 35 | func (c *Cacher) Set(key string, value types.ScoredDocuments) { 36 | c.client.Set(key, value, 5*time.Minute) 37 | } 38 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Go parameters 2 | GOCMD=go 3 | GOBUILD=$(GOCMD) build 4 | GOCLEAN=$(GOCMD) clean 5 | GOTEST=$(GOCMD) test 6 | GOGET=$(GOCMD) get 7 | BINARY_NAME=bin/example 8 | BINARY_UNIX=$(BINARY_NAME)_unix 9 | 10 | all: build-view build build-linux 11 | build-view: 12 | cd example/view && npm run build 13 | cd example && go-bindata-assetfs view/dist/... 14 | build: 15 | $(GOBUILD) -o $(BINARY_NAME) -v ./example/... 16 | clean: 17 | $(GOCLEAN) 18 | rm -f $(BINARY_NAME) 19 | rm -f $(BINARY_UNIX) 20 | rm -rf example/view/dist/* 21 | run-view: 22 | cd example/view && npm run dev 23 | run: 24 | $(GOBUILD) -o $(BINARY_NAME) -v example/main.go 25 | ./$(BINARY_NAME) 26 | analysis: 27 | @echo "engine" 28 | @ cloc --exclude-dir=example ./ 29 | @echo "example" 30 | @ cloc example/main.go 31 | @echo "view" 32 | @ cloc example/view/src 33 | 34 | # Cross compilation 35 | build-linux: 36 | CGO_ENABLED=0 GOOS=linux GOARCH=amd64 $(GOBUILD) -o $(BINARY_UNIX) ./example/... 37 | -------------------------------------------------------------------------------- /dbtest/redis_test.go: -------------------------------------------------------------------------------- 1 | package dbtest 2 | 3 | import ( 4 | "github.com/go-redis/redis" 5 | "github.com/sillydong/goczd/godata" 6 | "math/rand" 7 | "strconv" 8 | "testing" 9 | ) 10 | 11 | var rclient *redis.Client 12 | 13 | func init() { 14 | rclient = redis.NewClient(&redis.Options{ 15 | Addr: "127.0.0.1:6379", 16 | DB: 4, 17 | }) 18 | } 19 | 20 | func BenchmarkRAdd(b *testing.B) { 21 | for i := 0; i < b.N; i++ { 22 | lat, lng := RandomPoint() 23 | rclient.GeoAdd("geo", &redis.GeoLocation{ 24 | Name: strconv.Itoa(i), 25 | Longitude: lng, 26 | Latitude: lat, 27 | }) 28 | } 29 | } 30 | 31 | func BenchmarkRSearch(b *testing.B) { 32 | for i := 0; i < b.N; i++ { 33 | lat, lng := RandomPoint() 34 | rclient.GeoRadius("geo", lng, lat, &redis.GeoRadiusQuery{ 35 | Unit: "m", 36 | WithDist: true, 37 | Sort: "ASC", 38 | Count: 10, 39 | }) 40 | } 41 | } 42 | 43 | func RandomPoint() (lat, lng float64) { 44 | lat = 40.8137674 + godata.Round(rand.Float64()*0.01, 7) 45 | lng = -73.8525142 + godata.Round(rand.Float64()*0.01, 7) 46 | return 47 | } 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | LBSENGINE 2 | ----- 3 | 4 | ## What 5 | 6 | LBSENGINE实现了一个通用的GEO索引引擎,[2017Go基金会中国黑客马拉松](http://gohack2017.golangfoundation.org/)参赛项目,获得一等奖。 7 | 8 | ## How 9 | 10 | 项目成于2017Go基金会中国黑客马拉松,演示PPT见[lbsengine.pdf](https://github.com/sillydong/lbsengine/blob/master/lbsengine.pdf) 11 | 12 | 开发可参考`example`中的代码实现 13 | 14 | ## Who 15 | 16 | - [sillydong](https://github.com/sillydong) 17 | 18 | - [3zhen](https://github.com/3zheng) 19 | 20 | ## Todo 21 | 22 | 1. 维护一份内存索引,启动时从永久存储初始化内存索引 23 | 2. 优化序列化/反序列化方法 24 | 3. 参考Redis优化geohash算法 25 | 4. ... 26 | 27 | ## Thanks 28 | 29 | 1. [redis](http://github.com/go-redis/redis) 30 | 2. [geohash](http://github.com/mmcloughlin/geohash) 31 | 3. [go-cache](http://github.com/patrickmn/go-cache) 32 | 4. [murmur hash](https://github.com/huichen/murmur) 33 | 5. [gorp](https://github.com/go-gorp/gorp) 34 | 6. [echo](https://github.com/labstack/echo) 35 | 7. [go-bindata-assetfs](https://github.com/elazarl/go-bindata-assetfs) 36 | 8. [echo-static](https://github.com/Code-Hex/echo-static) 37 | 9. [msgp](https://github.com/tinylib/msgp) 38 | 10. [tidb](https://github.com/pingcap/tidb) 39 | -------------------------------------------------------------------------------- /core/geohash_test.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "github.com/mmcloughlin/geohash" 5 | "testing" 6 | ) 7 | 8 | func TestEncode(t *testing.T) { 9 | latitude := 31.286305 10 | longitude := 121.448463 11 | hash := geohash.EncodeWithPrecision(latitude, longitude, 5) 12 | t.Log(hash) 13 | } 14 | 15 | func TestEncodeInt(t *testing.T) { 16 | latitude := 31.286305 17 | longitude := 121.448463 18 | hash := geohash.EncodeIntWithPrecision(latitude, longitude, 6) 19 | t.Log(hash) 20 | } 21 | 22 | func TestNeighbour(t *testing.T) { 23 | hashs := geohash.Neighbors("wtw3g") 24 | t.Logf("%+v", hashs) 25 | } 26 | 27 | func BenchmarkNeighbour(b *testing.B) { 28 | for i := 0; i < b.N; i++ { 29 | geohash.Neighbors("wtw3g") 30 | } 31 | } 32 | 33 | func TestLoop(t *testing.T) { 34 | latitude := 31.286305 35 | longitude := 121.448463 36 | neightbours := LoopNeighbours(latitude, longitude, 5, 3) 37 | t.Logf("%+v", len(neightbours)) 38 | } 39 | 40 | func BenchmarkLoop(b *testing.B) { 41 | latitude := 31.286305 42 | longitude := 121.448463 43 | for i := 0; i < b.N; i++ { 44 | LoopNeighbours(latitude, longitude, 5, 1) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Reda Lemeden 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /engine/worker.go: -------------------------------------------------------------------------------- 1 | package engine 2 | 3 | import ( 4 | "github.com/sillydong/lbsengine/types" 5 | ) 6 | 7 | type indexerSearchRequest struct { 8 | countonly bool 9 | hash string 10 | latitude float64 11 | longitude float64 12 | option *types.SearchOptions 13 | indexerReturnChannel chan *indexerSearchResponse 14 | } 15 | 16 | type indexerSearchResponse struct { 17 | docs types.ScoredDocuments 18 | count int 19 | } 20 | 21 | func (e *Engine) indexerAddWorker(shard int) { 22 | for { 23 | request := <-e.indexerAddChannels[shard] 24 | e.indexer.Add(request) 25 | } 26 | } 27 | 28 | func (e *Engine) indexerRemoveWorker(shard int) { 29 | for { 30 | request := <-e.indexerRemoveChannels[shard] 31 | e.indexer.Remove(request) 32 | } 33 | } 34 | 35 | func (e *Engine) indexerSearchWorker(shard int) { 36 | for { 37 | request := <-e.indexerSearchChannels[shard] 38 | docs, count := e.indexer.Search(request.countonly, request.hash, request.latitude, request.longitude, request.option) 39 | request.indexerReturnChannel <- &indexerSearchResponse{docs: docs, count: count} 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /example/view/build/build.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | require('./check-versions')() 3 | 4 | process.env.NODE_ENV = 'production' 5 | 6 | const ora = require('ora') 7 | const rm = require('rimraf') 8 | const path = require('path') 9 | const chalk = require('chalk') 10 | const webpack = require('webpack') 11 | const config = require('../config') 12 | const webpackConfig = require('./webpack.prod.conf') 13 | 14 | const spinner = ora('building for production...') 15 | spinner.start() 16 | 17 | rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => { 18 | if (err) throw err 19 | webpack(webpackConfig, function (err, stats) { 20 | spinner.stop() 21 | if (err) throw err 22 | process.stdout.write(stats.toString({ 23 | colors: true, 24 | modules: false, 25 | children: false, 26 | chunks: false, 27 | chunkModules: false 28 | }) + '\n\n') 29 | 30 | if (stats.hasErrors()) { 31 | console.log(chalk.red(' Build failed with errors.\n')) 32 | process.exit(1) 33 | } 34 | 35 | console.log(chalk.cyan(' Build complete.\n')) 36 | console.log(chalk.yellow( 37 | ' Tip: built files are meant to be served over an HTTP server.\n' + 38 | ' Opening index.html over file:// won\'t work.\n' 39 | )) 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /example/view/build/webpack.dev.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const utils = require('./utils') 3 | const webpack = require('webpack') 4 | const config = require('../config') 5 | const merge = require('webpack-merge') 6 | const baseWebpackConfig = require('./webpack.base.conf') 7 | const HtmlWebpackPlugin = require('html-webpack-plugin') 8 | const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin') 9 | 10 | // add hot-reload related code to entry chunks 11 | Object.keys(baseWebpackConfig.entry).forEach(function (name) { 12 | baseWebpackConfig.entry[name] = ['./build/dev-client'].concat(baseWebpackConfig.entry[name]) 13 | }) 14 | 15 | module.exports = merge(baseWebpackConfig, { 16 | module: { 17 | rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap }) 18 | }, 19 | // cheap-module-eval-source-map is faster for development 20 | devtool: '#cheap-module-eval-source-map', 21 | plugins: [ 22 | new webpack.DefinePlugin({ 23 | 'process.env': config.dev.env 24 | }), 25 | // https://github.com/glenjamin/webpack-hot-middleware#installation--usage 26 | new webpack.HotModuleReplacementPlugin(), 27 | new webpack.NoEmitOnErrorsPlugin(), 28 | // https://github.com/ampedandwired/html-webpack-plugin 29 | new HtmlWebpackPlugin({ 30 | filename: 'index.html', 31 | template: 'index.html', 32 | inject: true 33 | }), 34 | new FriendlyErrorsPlugin() 35 | ] 36 | }) 37 | -------------------------------------------------------------------------------- /example/view/build/check-versions.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const chalk = require('chalk') 3 | const semver = require('semver') 4 | const packageConfig = require('../package.json') 5 | const shell = require('shelljs') 6 | function exec (cmd) { 7 | return require('child_process').execSync(cmd).toString().trim() 8 | } 9 | 10 | const versionRequirements = [ 11 | { 12 | name: 'node', 13 | currentVersion: semver.clean(process.version), 14 | versionRequirement: packageConfig.engines.node 15 | } 16 | ] 17 | 18 | if (shell.which('npm')) { 19 | versionRequirements.push({ 20 | name: 'npm', 21 | currentVersion: exec('npm --version'), 22 | versionRequirement: packageConfig.engines.npm 23 | }) 24 | } 25 | 26 | module.exports = function () { 27 | const warnings = [] 28 | for (let i = 0; i < versionRequirements.length; i++) { 29 | const mod = versionRequirements[i] 30 | if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) { 31 | warnings.push(mod.name + ': ' + 32 | chalk.red(mod.currentVersion) + ' should be ' + 33 | chalk.green(mod.versionRequirement) 34 | ) 35 | } 36 | } 37 | 38 | if (warnings.length) { 39 | console.log('') 40 | console.log(chalk.yellow('To use this template, you must update following to modules:')) 41 | console.log() 42 | for (let i = 0; i < warnings.length; i++) { 43 | const warning = warnings[i] 44 | console.log(' ' + warning) 45 | } 46 | console.log() 47 | process.exit(1) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /dbtest/tidb_test.go: -------------------------------------------------------------------------------- 1 | package dbtest 2 | 3 | import ( 4 | "database/sql" 5 | "github.com/go-gorp/gorp" 6 | "github.com/go-sql-driver/mysql" 7 | "log" 8 | "testing" 9 | "time" 10 | ) 11 | 12 | var client *gorp.DbMap 13 | 14 | func init() { 15 | dbconfig := &mysql.Config{ 16 | User: "root", 17 | Passwd: "", 18 | DBName: "lbsexample", 19 | Net: "tcp", 20 | Addr: "127.0.0.1:4000", 21 | } 22 | db, err := sql.Open("mysql", dbconfig.FormatDSN()) 23 | if err != nil { 24 | log.Fatal(err) 25 | } 26 | if err := db.Ping(); err != nil { 27 | log.Fatal(err) 28 | } 29 | db.SetConnMaxLifetime(time.Minute) 30 | db.SetMaxIdleConns(10) 31 | db.SetMaxOpenConns(10) 32 | 33 | client = &gorp.DbMap{Db: db, Dialect: gorp.MySQLDialect{}} 34 | 35 | client.AddTable(Tidbtest{}).SetKeys(true, "id") 36 | } 37 | 38 | type Tidbtest struct { 39 | Id int64 `db:"id"` 40 | Test string `db:"test"` 41 | } 42 | 43 | func TestAdd(t *testing.T) { 44 | data := &Tidbtest{Test: "hello"} 45 | err := client.Insert(data) 46 | if err != nil { 47 | t.Error(err) 48 | } else { 49 | t.Logf("%+v", data) 50 | } 51 | } 52 | 53 | func TestRemove(t *testing.T) { 54 | data := &Tidbtest{Id: 1} 55 | i, err := client.Delete(data) 56 | if err != nil { 57 | t.Error(err) 58 | } else { 59 | t.Log(i) 60 | } 61 | } 62 | 63 | func TestSelect(t *testing.T) { 64 | var datas []Tidbtest 65 | x, err := client.Select(&datas, "select * from tidbtest") 66 | if err != nil { 67 | t.Error(err) 68 | } else { 69 | t.Logf("%+v", x) 70 | t.Logf("%+v", datas) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /core/geohash.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import "github.com/mmcloughlin/geohash" 4 | 5 | func LoopNeighbours(latitude, longitude float64, precision uint, loop int) (neighbours []string) { 6 | if loop == 0 { 7 | loop = 1 8 | } 9 | hash := geohash.EncodeWithPrecision(latitude, longitude, precision) 10 | if loop == 1 { 11 | neighbours = geohash.Neighbors(hash) 12 | neighbours = append(neighbours, hash) 13 | return neighbours 14 | } else { 15 | neighbours = append(neighbours, hash) 16 | 17 | box := geohash.BoundingBox(hash) 18 | centerlat, centerlng := box.Center() 19 | height := box.MaxLat - box.MinLat 20 | width := box.MaxLng - box.MinLng 21 | 22 | for i := 1; i <= loop; i++ { 23 | side := 2 * i 24 | fi := float64(i) 25 | latup := centerlat + width*fi 26 | lngright := centerlng + width*fi 27 | latdown := centerlat - height*fi 28 | lngleft := centerlng - width*fi 29 | 30 | for k := 0; k < side; k++ { 31 | //上 32 | uplng := centerlng + width*float64(k-i+1) 33 | neighbours = append(neighbours, geohash.EncodeWithPrecision(latup, uplng, precision)) 34 | 35 | //右 36 | rightlat := centerlat - height*float64(k-i+1) 37 | neighbours = append(neighbours, geohash.EncodeWithPrecision(rightlat, lngright, precision)) 38 | 39 | //下 40 | downlng := centerlng - width*float64(k-i+1) 41 | neighbours = append(neighbours, geohash.EncodeWithPrecision(latdown, downlng, precision)) 42 | 43 | //左 44 | leftlat := centerlat + height*float64(k-i+1) 45 | neighbours = append(neighbours, geohash.EncodeWithPrecision(leftlat, lngleft, precision)) 46 | } 47 | } 48 | 49 | return neighbours 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /spider/jsonOperate.go: -------------------------------------------------------------------------------- 1 | package spider 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "reflect" 7 | "strconv" 8 | "strings" 9 | ) 10 | 11 | func ReadFromJson(body []byte) (datas []PoiData) { 12 | datas = make([]PoiData, 0) 13 | var dat map[string]interface{} 14 | dat = make(map[string]interface{}) 15 | err := json.Unmarshal(body, &dat) 16 | if err != nil { 17 | fmt.Println("json解析错误:", err) 18 | return 19 | } 20 | 21 | if suggestion, ok := dat["suggestion"]; ok { 22 | fmt.Println("type of suggestion: ", reflect.TypeOf(suggestion)) 23 | } 24 | 25 | if pois, ok := dat["pois"]; ok { 26 | fmt.Println("type of pois: ", reflect.TypeOf(pois)) 27 | switch data := pois.(type) { 28 | case []interface{}: 29 | fmt.Println("POI数组长度:", len(data)) 30 | for _, elem := range data { 31 | switch elem := elem.(type) { 32 | case map[string]interface{}: 33 | id, _ := elem["id"] 34 | name, _ := elem["name"] 35 | location, _ := elem["location"] 36 | fmt.Println(name, location) 37 | data := PoiData{} 38 | switch id := id.(type) { 39 | case string: 40 | data.ID = id 41 | } 42 | switch name := name.(type) { 43 | case string: 44 | data.Name = name 45 | } 46 | switch location := location.(type) { 47 | case string: 48 | data.Location = location 49 | coord := strings.Split(location, ",") 50 | data.Coordinate.Longitude, _ = strconv.ParseFloat(coord[0], 64) 51 | data.Coordinate.Latitude, _ = strconv.ParseFloat(coord[1], 64) 52 | } 53 | datas = append(datas, data) 54 | } 55 | } 56 | } 57 | } 58 | 59 | fmt.Println("datas =", datas) 60 | return 61 | } 62 | -------------------------------------------------------------------------------- /dbtest/Earthpoint_test.go: -------------------------------------------------------------------------------- 1 | package dbtest 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "testing" 7 | ) 8 | 9 | const RADIUS = 1.0 10 | 11 | type EarthCoordinate struct { 12 | longitude float64 //经度坐标 东经为正数,西经为负数 13 | latitude float64 //纬度坐标 北纬为正数,南纬为负数 14 | } 15 | 16 | func ChangeAngleToRadian(angle float64) float64 { 17 | return angle / 180.0 * math.Pi 18 | } 19 | 20 | func CalEarthPoint(pt1, pt2 EarthCoordinate) { 21 | x1 := RADIUS * math.Cos(ChangeAngleToRadian(pt1.latitude)) * math.Cos(ChangeAngleToRadian(pt1.longitude)) 22 | y1 := RADIUS * math.Cos(ChangeAngleToRadian(pt1.latitude)) * math.Sin(ChangeAngleToRadian(pt1.longitude)) 23 | z1 := RADIUS * math.Sin(ChangeAngleToRadian(pt1.latitude)) 24 | 25 | x2 := RADIUS * math.Cos(ChangeAngleToRadian(pt2.latitude)) * math.Cos(ChangeAngleToRadian(pt2.longitude)) 26 | y2 := RADIUS * math.Cos(ChangeAngleToRadian(pt2.latitude)) * math.Sin(ChangeAngleToRadian(pt2.longitude)) 27 | z2 := RADIUS * math.Sin(ChangeAngleToRadian(pt2.latitude)) 28 | 29 | distance := math.Sqrt(math.Pow(x1-x2, 2.0) + math.Pow(y1-y2, 2.0) + math.Pow(z1-z2, 2.0)) 30 | fmt.Printf("x1 = %f, y1 = %f, z1 = %f\nx2 = %f, y2 = %f, z2 = %f\ndistance = %f", x1, y1, z1, x2, y2, z2, distance) 31 | } 32 | 33 | func Test_EarthPoint(t *testing.T) { 34 | p1a, p2a := EarthCoordinate{longitude: 100.0, latitude: 20.0}, EarthCoordinate{longitude: 130.0, latitude: 40.0} 35 | p1b, p2b := EarthCoordinate{longitude: 170.0, latitude: 20.0}, EarthCoordinate{longitude: -160.0, latitude: 40.0} 36 | p1c, p2c := EarthCoordinate{longitude: 170.0, latitude: -20.0}, EarthCoordinate{longitude: -160.0, latitude: -40.0} 37 | CalEarthPoint(p1a, p2a) 38 | CalEarthPoint(p1b, p2b) 39 | CalEarthPoint(p1c, p2c) 40 | } 41 | -------------------------------------------------------------------------------- /types/options.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "runtime" 5 | "time" 6 | ) 7 | 8 | type EngineOptions struct { 9 | NumShards int //channel分片 10 | AddBuffer int //add channel长度 11 | RemoveBuffer int //remove channel长度 12 | SearchBuffer int //channel长度 13 | SearchWorkerThreads int //每个搜索channel worker数量 14 | 15 | DefaultSearchOption *SearchOptions //默认搜索配置 16 | IndexerOption *IndexerOptions //索引器配置项 17 | } 18 | 19 | func (o *EngineOptions) Init() { 20 | if o.NumShards == 0 { 21 | o.NumShards = runtime.NumCPU() 22 | } 23 | if o.AddBuffer == 0 { 24 | o.AddBuffer = runtime.NumCPU() 25 | } 26 | if o.RemoveBuffer == 0 { 27 | o.RemoveBuffer = runtime.NumCPU() 28 | } 29 | if o.SearchBuffer == 0 { 30 | o.SearchBuffer = 10 * runtime.NumCPU() 31 | } 32 | if o.SearchWorkerThreads == 0 { 33 | o.SearchWorkerThreads = 9 //9个neighbor格子 34 | } 35 | if o.DefaultSearchOption == nil { 36 | o.DefaultSearchOption = &SearchOptions{ 37 | Refresh: false, 38 | OrderDesc: false, 39 | Timeout: 2 * time.Second, 40 | Accuracy: STANDARD, 41 | Circles: 1, 42 | Excepts: nil, 43 | Filter: nil, 44 | } 45 | } 46 | if o.IndexerOption == nil { 47 | o.IndexerOption = &IndexerOptions{ 48 | RedisHost: "127.0.0.1:6379", 49 | RedisPassword: "", 50 | RedisDb: 3, 51 | HashSize: 1000, 52 | GeoShard: 5, 53 | GeoPrecious: 5, 54 | } 55 | } 56 | } 57 | 58 | type IndexerOptions struct { 59 | RedisHost string 60 | RedisPassword string 61 | RedisDb int 62 | HashSize uint64 //hash分片大小 63 | GeoShard uint64 //GEOHASH分片大小 64 | GeoPrecious uint //GEOHASH位数 65 | CenterLatitude float64 //城市中心纬度 66 | CenterLongitude float64 //城市中心经度 67 | Location string //城市 68 | } 69 | -------------------------------------------------------------------------------- /example/view/build/webpack.base.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const path = require('path') 3 | const utils = require('./utils') 4 | const config = require('../config') 5 | const vueLoaderConfig = require('./vue-loader.conf') 6 | 7 | function resolve (dir) { 8 | return path.join(__dirname, '..', dir) 9 | } 10 | 11 | module.exports = { 12 | entry: { 13 | app: './src/main.js' 14 | }, 15 | output: { 16 | path: config.build.assetsRoot, 17 | filename: '[name].js', 18 | publicPath: process.env.NODE_ENV === 'production' 19 | ? config.build.assetsPublicPath 20 | : config.dev.assetsPublicPath 21 | }, 22 | resolve: { 23 | extensions: ['.js', '.vue', '.json'], 24 | alias: { 25 | 'vue$': 'vue/dist/vue.esm.js', 26 | '@': resolve('src'), 27 | } 28 | }, 29 | module: { 30 | rules: [ 31 | { 32 | test: /\.vue$/, 33 | loader: 'vue-loader', 34 | options: vueLoaderConfig 35 | }, 36 | { 37 | test: /\.js$/, 38 | loader: 'babel-loader', 39 | include: [resolve('src'), resolve('test')] 40 | }, 41 | { 42 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, 43 | loader: 'url-loader', 44 | options: { 45 | limit: 10000, 46 | name: utils.assetsPath('img/[name].[hash:7].[ext]') 47 | } 48 | }, 49 | { 50 | test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/, 51 | loader: 'url-loader', 52 | options: { 53 | limit: 10000, 54 | name: utils.assetsPath('media/[name].[hash:7].[ext]') 55 | } 56 | }, 57 | { 58 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, 59 | loader: 'url-loader', 60 | options: { 61 | limit: 10000, 62 | name: utils.assetsPath('fonts/[name].[hash:7].[ext]') 63 | } 64 | } 65 | ] 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /example/view/config/index.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict' 3 | // Template version: 1.1.3 4 | // see http://vuejs-templates.github.io/webpack for documentation. 5 | 6 | const path = require('path') 7 | 8 | module.exports = { 9 | build: { 10 | env: require('./prod.env'), 11 | index: path.resolve(__dirname, '../dist/lbs.html'), 12 | assetsRoot: path.resolve(__dirname, '../dist'), 13 | assetsSubDirectory: 'static', 14 | assetsPublicPath: '/', 15 | productionSourceMap: true, 16 | // Gzip off by default as many popular static hosts such as 17 | // Surge or Netlify already gzip all static assets for you. 18 | // Before setting to `true`, make sure to: 19 | // npm install --save-dev compression-webpack-plugin 20 | productionGzip: false, 21 | productionGzipExtensions: ['js', 'css'], 22 | // Run the build command with an extra argument to 23 | // View the bundle analyzer report after build finishes: 24 | // `npm run build --report` 25 | // Set to `true` or `false` to always turn it on or off 26 | bundleAnalyzerReport: process.env.npm_config_report 27 | }, 28 | dev: { 29 | env: require('./dev.env'), 30 | port: process.env.PORT || 8080, 31 | autoOpenBrowser: true, 32 | assetsSubDirectory: 'static', 33 | assetsPublicPath: '/', 34 | proxyTable: { 35 | '/api':{ 36 | target:'http://localhost:8877', 37 | changeOrigin: true, 38 | logLeve: 'debug', 39 | pathRewrite:{ 40 | '^/api':'/api' 41 | } 42 | } 43 | }, 44 | // CSS Sourcemaps off by default because relative paths are "buggy" 45 | // with this option, according to the CSS-Loader README 46 | // (https://github.com/webpack/css-loader#sourcemaps) 47 | // In our experience, they generally work as expected, 48 | // just be aware of this issue when enabling this option. 49 | cssSourceMap: false 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /example/view/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "view", 3 | "version": "1.0.0", 4 | "description": "A Vue.js project", 5 | "author": "Chen.Zhidong ", 6 | "private": true, 7 | "scripts": { 8 | "dev": "node build/dev-server.js", 9 | "start": "npm run dev", 10 | "build": "node build/build.js" 11 | }, 12 | "dependencies": { 13 | "axios": "^0.16.2", 14 | "mint-ui": "^2.2.9", 15 | "qs": "^6.5.1", 16 | "vue": "^2.5.2", 17 | "vue-amap": "^0.3.1", 18 | "vue-router": "^3.0.1" 19 | }, 20 | "devDependencies": { 21 | "autoprefixer": "^7.1.2", 22 | "babel-core": "^6.22.1", 23 | "babel-loader": "^7.1.1", 24 | "babel-plugin-transform-runtime": "^6.22.0", 25 | "babel-preset-env": "^1.3.2", 26 | "babel-preset-stage-2": "^6.22.0", 27 | "babel-register": "^6.22.0", 28 | "chalk": "^2.0.1", 29 | "connect-history-api-fallback": "^1.3.0", 30 | "copy-webpack-plugin": "^4.0.1", 31 | "css-loader": "^0.28.0", 32 | "eventsource-polyfill": "^0.9.6", 33 | "express": "^4.14.1", 34 | "extract-text-webpack-plugin": "^3.0.0", 35 | "file-loader": "^1.1.4", 36 | "friendly-errors-webpack-plugin": "^1.6.1", 37 | "html-webpack-plugin": "^2.30.1", 38 | "http-proxy-middleware": "^0.17.3", 39 | "webpack-bundle-analyzer": "^2.9.0", 40 | "semver": "^5.3.0", 41 | "shelljs": "^0.7.6", 42 | "opn": "^5.1.0", 43 | "optimize-css-assets-webpack-plugin": "^3.2.0", 44 | "ora": "^1.2.0", 45 | "rimraf": "^2.6.0", 46 | "url-loader": "^0.5.8", 47 | "vue-loader": "^13.3.0", 48 | "vue-style-loader": "^3.0.1", 49 | "vue-template-compiler": "^2.5.2", 50 | "portfinder": "^1.0.13", 51 | "webpack": "^3.6.0", 52 | "webpack-dev-middleware": "^1.12.0", 53 | "webpack-hot-middleware": "^2.18.2", 54 | "webpack-merge": "^4.1.0" 55 | }, 56 | "engines": { 57 | "node": ">= 4.0.0", 58 | "npm": ">= 3.0.0" 59 | }, 60 | "browserslist": [ 61 | "> 1%", 62 | "last 2 versions", 63 | "not ie <= 8" 64 | ] 65 | } 66 | -------------------------------------------------------------------------------- /example/view/build/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const path = require('path') 3 | const config = require('../config') 4 | const ExtractTextPlugin = require('extract-text-webpack-plugin') 5 | 6 | exports.assetsPath = function (_path) { 7 | const assetsSubDirectory = process.env.NODE_ENV === 'production' 8 | ? config.build.assetsSubDirectory 9 | : config.dev.assetsSubDirectory 10 | return path.posix.join(assetsSubDirectory, _path) 11 | } 12 | 13 | exports.cssLoaders = function (options) { 14 | options = options || {} 15 | 16 | const cssLoader = { 17 | loader: 'css-loader', 18 | options: { 19 | minimize: process.env.NODE_ENV === 'production', 20 | sourceMap: options.sourceMap 21 | } 22 | } 23 | 24 | // generate loader string to be used with extract text plugin 25 | function generateLoaders (loader, loaderOptions) { 26 | const loaders = [cssLoader] 27 | if (loader) { 28 | loaders.push({ 29 | loader: loader + '-loader', 30 | options: Object.assign({}, loaderOptions, { 31 | sourceMap: options.sourceMap 32 | }) 33 | }) 34 | } 35 | 36 | // Extract CSS when that option is specified 37 | // (which is the case during production build) 38 | if (options.extract) { 39 | return ExtractTextPlugin.extract({ 40 | use: loaders, 41 | fallback: 'vue-style-loader' 42 | }) 43 | } else { 44 | return ['vue-style-loader'].concat(loaders) 45 | } 46 | } 47 | 48 | // https://vue-loader.vuejs.org/en/configurations/extract-css.html 49 | return { 50 | css: generateLoaders(), 51 | postcss: generateLoaders(), 52 | less: generateLoaders('less'), 53 | sass: generateLoaders('sass', { indentedSyntax: true }), 54 | scss: generateLoaders('sass'), 55 | stylus: generateLoaders('stylus'), 56 | styl: generateLoaders('stylus') 57 | } 58 | } 59 | 60 | // Generate loaders for standalone style files (outside of .vue) 61 | exports.styleLoaders = function (options) { 62 | const output = [] 63 | const loaders = exports.cssLoaders(options) 64 | for (const extension in loaders) { 65 | const loader = loaders[extension] 66 | output.push({ 67 | test: new RegExp('\\.' + extension + '$'), 68 | use: loader 69 | }) 70 | } 71 | return output 72 | } 73 | -------------------------------------------------------------------------------- /engine/engine_test.go: -------------------------------------------------------------------------------- 1 | package engine 2 | 3 | import ( 4 | "fmt" 5 | "github.com/huichen/murmur" 6 | "github.com/sillydong/goczd/godata" 7 | "github.com/sillydong/lbsengine/types" 8 | "math/rand" 9 | "testing" 10 | "time" 11 | "unsafe" 12 | ) 13 | 14 | var e *Engine 15 | 16 | func init() { 17 | rand.Seed(time.Now().Unix()) 18 | e = &Engine{} 19 | e.Init(nil) 20 | } 21 | 22 | func BenchmarkAdd(b *testing.B) { 23 | for i := 0; i < b.N; i++ { 24 | lat, lng := RandomPoint() 25 | e.Add(&types.IndexedDocument{ 26 | DocId: uint64(i), 27 | Latitude: lat, 28 | Longitude: lng, 29 | Fields: map[string]string{ 30 | "a": "b", 31 | }, 32 | }) 33 | } 34 | } 35 | 36 | func TestSearch(t *testing.T) { 37 | lat, lng := RandomPoint() 38 | fmt.Println(lat, lng) 39 | result := e.Search(&types.SearchRequest{ 40 | Latitude: lat, 41 | Longitude: lng, 42 | Offset: 0, 43 | Limit: 100, 44 | }) 45 | fmt.Printf("%+v\n", result) 46 | //x,_ := json.Marshal(result) 47 | //fmt.Println(string(x)) 48 | } 49 | 50 | func BenchmarkSearch(b *testing.B) { 51 | for i := 0; i < b.N; i++ { 52 | lat, lng := RandomPoint() 53 | e.Search(&types.SearchRequest{ 54 | Latitude: lat, 55 | Longitude: lng, 56 | Offset: 0, 57 | Limit: 10, 58 | //SearchOption:&types.SearchOptions{ 59 | // Refresh:false, 60 | // Circles:2, 61 | //}, 62 | }) 63 | //fmt.Println(resp.Count) 64 | } 65 | } 66 | 67 | func TestPoint(t *testing.T) { 68 | lat, lng := RandomPoint() 69 | fmt.Printf("%+v - %+v", lat, lng) 70 | } 71 | 72 | func RandomPoint() (lat, lng float64) { 73 | lat = 40.8137674 + godata.Round(rand.Float64()*0.01, 7) 74 | lng = -73.8525142 + godata.Round(rand.Float64()*0.01, 7) 75 | return 76 | } 77 | 78 | func TestParse(t *testing.T) { 79 | t.Logf("%+v", []byte(fmt.Sprintf("%d", 1234567890))) 80 | t.Logf("%+v", parse(1234567890)) 81 | } 82 | 83 | func BenchmarkStringShard(b *testing.B) { 84 | for i := 0; i < b.N; i++ { 85 | x := uint64(i) 86 | murmur.Murmur3([]byte(fmt.Sprintf("%d", x))) 87 | } 88 | } 89 | 90 | func BenchmarkUintShard(b *testing.B) { 91 | for i := 0; i < b.N; i++ { 92 | x := uint64(i) 93 | murmur.Murmur3(parse(x)) 94 | } 95 | } 96 | 97 | func parse(d uint64) []byte { 98 | s := fmt.Sprintf("%d", d) 99 | x := (*[2]uintptr)(unsafe.Pointer(&s)) 100 | h := [3]uintptr{x[0], x[1], x[1]} 101 | return *(*[]byte)(unsafe.Pointer(&h)) 102 | } 103 | -------------------------------------------------------------------------------- /spider/spider.go: -------------------------------------------------------------------------------- 1 | package spider 2 | 3 | import ( 4 | "crypto/md5" 5 | "fmt" 6 | "github.com/sillydong/lbsengine/distanceMeasure" 7 | "io/ioutil" 8 | "net/http" 9 | "sort" 10 | ) 11 | 12 | type PoiData struct { 13 | ID string //唯一标识符 14 | Name string //兴趣点的名字 15 | Location string //字符串形式的坐标 16 | Coordinate distanceMeasure.EarthCoordinate //EarthCoordinate形式的坐标 17 | } 18 | 19 | type URL struct { 20 | UrlHead string //url的开始部分,类似http://restapi.amap.com/v3/place/text? 21 | MapParam map[string]string //url的参数部分 22 | } 23 | 24 | func (this *URL) Init(urlHead string) { 25 | this.MapParam = make(map[string]string, 50) 26 | this.UrlHead = urlHead 27 | } 28 | 29 | func (this *URL) AddParam(key, value string) { 30 | this.MapParam[key] = value 31 | } 32 | 33 | func (this *URL) GetFinalURL() string { 34 | str := this.UrlHead 35 | for key, value := range this.MapParam { 36 | str += "&" + key + "=" + value 37 | } 38 | return str 39 | } 40 | 41 | //根据已有的参数获取数字签名的MD5值,privateKey为私钥 42 | func (this *URL) GetMD5Sign(privateKey string) string { 43 | keys := make([]string, 0) 44 | for key, _ := range this.MapParam { 45 | keys = append(keys, key) 46 | } 47 | //升序排序 48 | sort.Strings(keys) 49 | //根据排序后的结果取key-value值 50 | var sign string 51 | length := len(keys) 52 | for i, key := range keys { 53 | elem, _ := this.MapParam[key] 54 | sign += key + "=" + elem 55 | if i < length-1 { 56 | //不是最后一个的话要+& 57 | sign += "&" 58 | } else { 59 | //最后一个则直接+私钥 60 | sign += privateKey 61 | } 62 | } 63 | //进行MD5加密 64 | fmt.Println("被加密的MD5串:", sign) 65 | byteMD5 := md5.Sum([]byte(sign)) 66 | sigMD5 := fmt.Sprintf("%x", byteMD5) 67 | return sigMD5 68 | } 69 | 70 | func GetPOIData(pageId string) []PoiData { 71 | //var url string = "http://restapi.amap.com/v3/place/text?&keywords=超市&city=shanghai&key=7ffc2abae565cdb9302ebaef2e45c572&extensions=all&sig=" 72 | pUrl := new(URL) 73 | pUrl.Init("http://restapi.amap.com/v3/place/text?") 74 | pUrl.AddParam("keywords", "超市") 75 | pUrl.AddParam("city", "shanghai") 76 | pUrl.AddParam("key", "7ffc2abae565cdb9302ebaef2e45c572") 77 | pUrl.AddParam("extensions", "all") 78 | pUrl.AddParam("page", pageId) 79 | //获取数字签名的MD5值 80 | sig := pUrl.GetMD5Sign("f66d435f57cb361630f2110a97aa4fd9") 81 | pUrl.AddParam("sig", sig) 82 | finalUrl := pUrl.GetFinalURL() 83 | fmt.Println("final url: ", finalUrl) 84 | resp, err := http.Get(finalUrl) 85 | if err != nil { 86 | fmt.Println("post错误:", err) 87 | return nil 88 | } 89 | fmt.Println("resp结果值:", resp) 90 | 91 | body, err := ioutil.ReadAll(resp.Body) 92 | fmt.Println("返回的body为:", string(body[:])) 93 | return ReadFromJson(body) 94 | } 95 | -------------------------------------------------------------------------------- /example/view/src/utils/fetch.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import {MessageBox,Indicator} from "mint-ui"; 3 | import router from "../router/index" 4 | import * as qs from "qs"; 5 | 6 | // 创建axios实例 7 | const service = axios.create({ 8 | baseURL: process.env.BASE_API, // api的base_url 9 | timeout: 5000, // 请求超时时间 10 | }); 11 | 12 | // request拦截器 13 | service.interceptors.request.use((config) => { 14 | Indicator.open(); 15 | // Do something before request is sent 16 | config.headers.post['Content-Type'] = 'application/x-www-form-urlencoded'; 17 | if (config.method === 'post') { 18 | config.data = qs.stringify(config.data); 19 | } 20 | return config; 21 | }, (error) => { 22 | // Do something with request error 23 | console.log(error); // for debug 24 | MessageBox.alert("请求失败5",error); 25 | return Promise.reject(error); 26 | }); 27 | 28 | // respone拦截器 29 | service.interceptors.response.use( 30 | (response) => { 31 | Indicator.close() 32 | console.log(response.data) 33 | const status = response.data.status; 34 | if (status == 1) { 35 | return response 36 | } else { 37 | const err = response.data.error; 38 | switch (typeof(err)) { 39 | case "string": 40 | MessageBox.alert("请求失败4", err); 41 | break; 42 | case "object": 43 | MessageBox.alert("请求失败3", Object.values(err).join()); 44 | break; 45 | case "array": 46 | MessageBox.alert("请求失败2", Object.values(err).join()); 47 | break; 48 | } 49 | return Promise.reject(response); 50 | } 51 | }, error => { 52 | Indicator.close(); 53 | console.log('err' + error);// for debug 54 | MessageBox.alert("请求失败1", error.message); 55 | return Promise.reject(error); 56 | } 57 | ); 58 | 59 | export function get(url, params) { 60 | return new Promise((resolve, reject) => { 61 | service.get(url, {params: params}).then(response => { 62 | resolve(response.data); 63 | }, err => { 64 | reject(err) 65 | }).catch((error) => { 66 | reject(error) 67 | }); 68 | }) 69 | } 70 | 71 | export function post(url, params) { 72 | return new Promise((resolve, reject) => { 73 | service.post(url, params).then(response => { 74 | resolve(response.data); 75 | }, err => { 76 | reject(err) 77 | }).catch((error) => { 78 | reject(error) 79 | }); 80 | }) 81 | } 82 | 83 | export function del(url){ 84 | return new Promise((resolve,reject)=>{ 85 | service.del(url).then(response => { 86 | resolve(response.data); 87 | },err=>{ 88 | reject(err) 89 | }).catch(err => { 90 | reject(err) 91 | }); 92 | }) 93 | } 94 | -------------------------------------------------------------------------------- /example/view/src/views/index.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 93 | 94 | 105 | -------------------------------------------------------------------------------- /core/indexer_test.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "fmt" 5 | "github.com/sillydong/lbsengine/types" 6 | "math/rand" 7 | "testing" 8 | ) 9 | 10 | var indexer *Indexer 11 | 12 | func init() { 13 | indexer = &Indexer{} 14 | indexer.Init(&types.IndexerOptions{ 15 | RedisHost: "127.0.0.1:6379", 16 | RedisDb: 3, 17 | HashSize: 1000, 18 | GeoShard: 5, 19 | GeoPrecious: 5, 20 | }) 21 | } 22 | 23 | func TestAddDocument(t *testing.T) { 24 | doc := &types.IndexedDocument{ 25 | DocId: 1, 26 | Latitude: 40.7137674, 27 | Longitude: -73.9525142, 28 | Fields: map[string]string{ 29 | "a": "b", 30 | "c": "d", 31 | }, 32 | } 33 | indexer.Add(doc) 34 | } 35 | 36 | func TestRemoveDocument(t *testing.T) { 37 | indexer.Remove(1) 38 | } 39 | 40 | func TestStore(t *testing.T) { 41 | strs, err := indexer.client.HVals("h_dr5rt_1").Result() 42 | if err != nil { 43 | t.Error(err) 44 | } 45 | docs := make([]types.IndexedDocument, 0) 46 | for _, str := range strs { 47 | document := types.IndexedDocument{} 48 | document.UnmarshalMsg([]byte(str)) 49 | docs = append(docs, document) 50 | } 51 | fmt.Printf("%+v\n", docs) 52 | } 53 | 54 | func BenchmarkStore(b *testing.B) { 55 | str, _ := indexer.client.HGet("h_dr5rt_1", "1").Result() 56 | for i := 0; i < b.N; i++ { 57 | document := types.IndexedDocument{} 58 | document.UnmarshalMsg(tobytes(str)) 59 | } 60 | } 61 | 62 | func TestSearch(t *testing.T) { 63 | option := &types.SearchOptions{ 64 | Refresh: false, 65 | OrderDesc: false, 66 | Accuracy: types.STANDARD, 67 | Circles: 1, 68 | Excepts: map[uint64]bool{ 69 | 1: true, 70 | }, 71 | Filter: func(doc types.IndexedDocument) bool { 72 | fmt.Println("filtering") 73 | if doc.Fields.(map[string]interface{})["a"] == "b" { 74 | return true 75 | } 76 | return false 77 | }, 78 | } 79 | docs, num := indexer.Search(false, "h_dr5rt_1", 40.7137674, -73.9525142, option) 80 | fmt.Println(num) 81 | if num > 0 { 82 | for _, doc := range docs { 83 | fmt.Printf("%+v\n", doc) 84 | } 85 | } 86 | } 87 | 88 | func BenchmarkSearch(b *testing.B) { 89 | option := &types.SearchOptions{ 90 | Refresh: false, 91 | OrderDesc: false, 92 | Accuracy: types.MEITUAN, 93 | Circles: 1, 94 | //Excepts: map[uint64]bool{ 95 | // 1: true, 96 | //}, 97 | //Filter: func(doc types.IndexedDocument) bool { 98 | // fmt.Println("filtering") 99 | // if doc.Fields.(map[string]interface{})["a"] == "b" { 100 | // return true 101 | // } 102 | // return false 103 | //}, 104 | } 105 | for i := 0; i < b.N; i++ { 106 | indexer.Search(false, "h_dr5rt_1", 40.7137674, -73.9525142, option) 107 | } 108 | } 109 | 110 | func TestIdShard(t *testing.T) { 111 | for i := 0; i < 10; i++ { 112 | x := rand.Int() 113 | fmt.Printf("%v -> %v\n", x, indexer.hashshard(uint64(x))) 114 | } 115 | } 116 | 117 | func TestGeoShard(t *testing.T) { 118 | for i := 0; i < 10; i++ { 119 | fmt.Printf("%v -> %v\n", i, indexer.geoshard("asdfg", uint64(i))) 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /types/indexed_document_gen_test.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | // NOTE: THIS FILE WAS PRODUCED BY THE 4 | // MSGP CODE GENERATION TOOL (github.com/tinylib/msgp) 5 | // DO NOT EDIT 6 | 7 | import ( 8 | "bytes" 9 | "testing" 10 | 11 | "github.com/tinylib/msgp/msgp" 12 | ) 13 | 14 | func TestMarshalUnmarshalIndexedDocument(t *testing.T) { 15 | v := IndexedDocument{} 16 | bts, err := v.MarshalMsg(nil) 17 | if err != nil { 18 | t.Fatal(err) 19 | } 20 | left, err := v.UnmarshalMsg(bts) 21 | if err != nil { 22 | t.Fatal(err) 23 | } 24 | if len(left) > 0 { 25 | t.Errorf("%d bytes left over after UnmarshalMsg(): %q", len(left), left) 26 | } 27 | 28 | left, err = msgp.Skip(bts) 29 | if err != nil { 30 | t.Fatal(err) 31 | } 32 | if len(left) > 0 { 33 | t.Errorf("%d bytes left over after Skip(): %q", len(left), left) 34 | } 35 | } 36 | 37 | func BenchmarkMarshalMsgIndexedDocument(b *testing.B) { 38 | v := IndexedDocument{} 39 | b.ReportAllocs() 40 | b.ResetTimer() 41 | for i := 0; i < b.N; i++ { 42 | v.MarshalMsg(nil) 43 | } 44 | } 45 | 46 | func BenchmarkAppendMsgIndexedDocument(b *testing.B) { 47 | v := IndexedDocument{} 48 | bts := make([]byte, 0, v.Msgsize()) 49 | bts, _ = v.MarshalMsg(bts[0:0]) 50 | b.SetBytes(int64(len(bts))) 51 | b.ReportAllocs() 52 | b.ResetTimer() 53 | for i := 0; i < b.N; i++ { 54 | bts, _ = v.MarshalMsg(bts[0:0]) 55 | } 56 | } 57 | 58 | func BenchmarkUnmarshalIndexedDocument(b *testing.B) { 59 | v := IndexedDocument{} 60 | bts, _ := v.MarshalMsg(nil) 61 | b.ReportAllocs() 62 | b.SetBytes(int64(len(bts))) 63 | b.ResetTimer() 64 | for i := 0; i < b.N; i++ { 65 | _, err := v.UnmarshalMsg(bts) 66 | if err != nil { 67 | b.Fatal(err) 68 | } 69 | } 70 | } 71 | 72 | func TestEncodeDecodeIndexedDocument(t *testing.T) { 73 | v := IndexedDocument{} 74 | var buf bytes.Buffer 75 | msgp.Encode(&buf, &v) 76 | 77 | m := v.Msgsize() 78 | if buf.Len() > m { 79 | t.Logf("WARNING: Msgsize() for %v is inaccurate", v) 80 | } 81 | 82 | vn := IndexedDocument{} 83 | err := msgp.Decode(&buf, &vn) 84 | if err != nil { 85 | t.Error(err) 86 | } 87 | 88 | buf.Reset() 89 | msgp.Encode(&buf, &v) 90 | err = msgp.NewReader(&buf).Skip() 91 | if err != nil { 92 | t.Error(err) 93 | } 94 | } 95 | 96 | func BenchmarkEncodeIndexedDocument(b *testing.B) { 97 | v := IndexedDocument{} 98 | var buf bytes.Buffer 99 | msgp.Encode(&buf, &v) 100 | b.SetBytes(int64(buf.Len())) 101 | en := msgp.NewWriter(msgp.Nowhere) 102 | b.ReportAllocs() 103 | b.ResetTimer() 104 | for i := 0; i < b.N; i++ { 105 | v.EncodeMsg(en) 106 | } 107 | en.Flush() 108 | } 109 | 110 | func BenchmarkDecodeIndexedDocument(b *testing.B) { 111 | v := IndexedDocument{} 112 | var buf bytes.Buffer 113 | msgp.Encode(&buf, &v) 114 | b.SetBytes(int64(buf.Len())) 115 | rd := msgp.NewEndlessReader(buf.Bytes(), b) 116 | dc := msgp.NewReader(rd) 117 | b.ReportAllocs() 118 | b.ResetTimer() 119 | for i := 0; i < b.N; i++ { 120 | err := v.DecodeMsg(dc) 121 | if err != nil { 122 | b.Fatal(err) 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /example/view/build/dev-server.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | require('./check-versions')() 3 | 4 | const config = require('../config') 5 | if (!process.env.NODE_ENV) { 6 | process.env.NODE_ENV = JSON.parse(config.dev.env.NODE_ENV) 7 | } 8 | 9 | const opn = require('opn') 10 | const path = require('path') 11 | const express = require('express') 12 | const webpack = require('webpack') 13 | const proxyMiddleware = require('http-proxy-middleware') 14 | const webpackConfig = require('./webpack.dev.conf') 15 | 16 | // default port where dev server listens for incoming traffic 17 | const port = process.env.PORT || config.dev.port 18 | // automatically open browser, if not set will be false 19 | const autoOpenBrowser = !!config.dev.autoOpenBrowser 20 | // Define HTTP proxies to your custom API backend 21 | // https://github.com/chimurai/http-proxy-middleware 22 | const proxyTable = config.dev.proxyTable 23 | 24 | const app = express() 25 | const compiler = webpack(webpackConfig) 26 | 27 | const devMiddleware = require('webpack-dev-middleware')(compiler, { 28 | publicPath: webpackConfig.output.publicPath, 29 | quiet: true 30 | }) 31 | 32 | const hotMiddleware = require('webpack-hot-middleware')(compiler, { 33 | log: false, 34 | heartbeat: 2000 35 | }) 36 | // force page reload when html-webpack-plugin template changes 37 | // currently disabled until this is resolved: 38 | // https://github.com/jantimon/html-webpack-plugin/issues/680 39 | // compiler.plugin('compilation', function (compilation) { 40 | // compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) { 41 | // hotMiddleware.publish({ action: 'reload' }) 42 | // cb() 43 | // }) 44 | // }) 45 | 46 | // enable hot-reload and state-preserving 47 | // compilation error display 48 | app.use(hotMiddleware) 49 | 50 | // proxy api requests 51 | Object.keys(proxyTable).forEach(function (context) { 52 | let options = proxyTable[context] 53 | if (typeof options === 'string') { 54 | options = { target: options } 55 | } 56 | app.use(proxyMiddleware(options.filter || context, options)) 57 | }) 58 | 59 | // handle fallback for HTML5 history API 60 | app.use(require('connect-history-api-fallback')()) 61 | 62 | // serve webpack bundle output 63 | app.use(devMiddleware) 64 | 65 | // serve pure static assets 66 | const staticPath = path.posix.join(config.dev.assetsPublicPath, config.dev.assetsSubDirectory) 67 | app.use(staticPath, express.static('./static')) 68 | 69 | const uri = 'http://localhost:' + port 70 | 71 | var _resolve 72 | var _reject 73 | var readyPromise = new Promise((resolve, reject) => { 74 | _resolve = resolve 75 | _reject = reject 76 | }) 77 | 78 | var server 79 | var portfinder = require('portfinder') 80 | portfinder.basePort = port 81 | 82 | console.log('> Starting dev server...') 83 | devMiddleware.waitUntilValid(() => { 84 | portfinder.getPort((err, port) => { 85 | if (err) { 86 | _reject(err) 87 | } 88 | process.env.PORT = port 89 | var uri = 'http://localhost:' + port 90 | console.log('> Listening at ' + uri + '\n') 91 | // when env is testing, don't need open it 92 | if (autoOpenBrowser && process.env.NODE_ENV !== 'testing') { 93 | opn(uri) 94 | } 95 | server = app.listen(port) 96 | _resolve() 97 | }) 98 | }) 99 | 100 | module.exports = { 101 | ready: readyPromise, 102 | close: () => { 103 | server.close() 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /distanceMeasure/distanceMeasure_test.go: -------------------------------------------------------------------------------- 1 | package distanceMeasure 2 | 3 | import ( 4 | "log" 5 | "math" 6 | "math/rand" 7 | "os" 8 | "testing" 9 | "time" 10 | ) 11 | 12 | const TEST_DATA_NUM = 9000000 13 | 14 | var arrPoint1 [TEST_DATA_NUM]*EarthCoordinate //测试数据组1 15 | var arrPoint2 [TEST_DATA_NUM]*EarthCoordinate //测试数据组2 16 | var precision float64 = 0.1 //随机数生成的精度大小 17 | var benchmarkPoint EarthCoordinate = EarthCoordinate{Longitude: 121.0, Latitude: 31.0} //基准值 18 | var resultStardard, resultQuick, resultQuick2 [TEST_DATA_NUM]float64 //各个算法的测距后得到的结果 19 | 20 | func init() { 21 | //使用随机数生成测试数据 22 | //var tmpPoint EarthCoordinate 23 | now := time.Now().Unix() 24 | rand.Seed(now) 25 | 26 | for i := 0; i < TEST_DATA_NUM; i++ { 27 | //随机生成1000000组经纬度坐标 28 | //以1000000为除数求余,然后把得到的余数再与除以1000000作为benchmarkPoint的小数部分 29 | tmpPoint := new(EarthCoordinate) 30 | tmpPoint.Latitude = benchmarkPoint.Latitude + float64(rand.Intn(1000000))/1000000*precision 31 | tmpPoint.Longitude = benchmarkPoint.Longitude + float64(rand.Intn(1000000))/1000000*precision 32 | arrPoint1[i] = tmpPoint 33 | tmpPoint = new(EarthCoordinate) 34 | tmpPoint.Latitude = benchmarkPoint.Latitude + float64(rand.Intn(1000000))/1000000*precision 35 | tmpPoint.Longitude = benchmarkPoint.Longitude + float64(rand.Intn(1000000))/1000000*precision 36 | arrPoint2[i] = tmpPoint 37 | } 38 | } 39 | 40 | //标准球体算法 41 | func TestStardard(t *testing.T) { 42 | measure := GetInstance() 43 | //计算标准三维距离算法的时间 44 | before := time.Now().UnixNano() 45 | for i := 0; i < TEST_DATA_NUM; i++ { 46 | resultStardard[i] = measure.MeasureByStardardMethod(arrPoint1[i], arrPoint2[i]) 47 | } 48 | after := time.Now().UnixNano() 49 | t.Logf("Haversine公式标准球体算法耗时:%E ns\n", float64(after-before)) 50 | } 51 | 52 | //没有预设本地经纬度坐标的快速测距算法 53 | func TestQuickWithoutSetLocation(t *testing.T) { 54 | measure := GetInstance() 55 | //通过三维近似为二维计算距离算法的时间 56 | before := time.Now().UnixNano() 57 | for i := 0; i < TEST_DATA_NUM; i++ { 58 | resultQuick[i] = measure.MeasureByQuickMethodWithoutLocation(arrPoint1[i], arrPoint2[i]) 59 | } 60 | after := time.Now().UnixNano() 61 | t.Logf("平面近似二维距离算法(勾股定理,未预设城市经纬度)的耗时:%E ns\n", float64(after-before)) 62 | } 63 | 64 | func TestQuick(t *testing.T) { 65 | measure := GetInstance() 66 | //先来个错误的示范 67 | if _, err := measure.MeasureByQuickMethod(arrPoint1[0], arrPoint2[0]); err != nil { 68 | t.Log(err) 69 | } 70 | 71 | //通过三维近似为二维计算距离算法的时间 72 | before := time.Now().UnixNano() 73 | measure.SetLocalEarthCoordinate(&benchmarkPoint, "上海") 74 | for i := 0; i < TEST_DATA_NUM; i++ { 75 | if value, err := measure.MeasureByQuickMethod(arrPoint1[i], arrPoint2[i]); err != nil { 76 | t.Error("出错了") 77 | } else { 78 | resultQuick2[i] = value 79 | } 80 | } 81 | after := time.Now().UnixNano() 82 | t.Logf("平面近似二维距离算法(勾股定理,已预设城市经纬度)的耗时:%E ns\n", float64(after-before)) 83 | } 84 | 85 | //计算标准差 86 | func TestCompare(t *testing.T) { 87 | var variance, variance2 float64 = 0.0, 0.0 88 | logFile, err := os.OpenFile("distance.log", os.O_APPEND|os.O_CREATE, 0666) 89 | if err != nil { 90 | t.Log("打开distance.log失败") 91 | return 92 | } 93 | log.SetOutput(logFile) 94 | for i := 0; i < TEST_DATA_NUM/100; i++ { 95 | log.Printf("第%d组数据: EarthPointPair = %v 和 %v, stardard = %f, quick = %f, quick2 = %f\n", 96 | i, arrPoint1[i], arrPoint2[i], resultStardard[i], resultQuick[i], resultQuick[i]) 97 | } 98 | 99 | for i := 0; i < TEST_DATA_NUM; i++ { 100 | // log.Printf("第%d组数据: EarthPointPair = %v, stardard = %f, quick = %f, quick2 = %f\n", 101 | // i, arrPoint[i], resultStardard[i], resultQuick[i], resultQuick[i]) 102 | variance += math.Pow(resultQuick[i]-resultStardard[i], 2) 103 | variance2 += math.Pow(resultQuick2[i]-resultStardard[i], 2) 104 | } 105 | t.Log(variance, variance2) 106 | t.Logf("精度为%f度时,换算以米为单位时表示搜索范围距离在%f米内时:\n标准差组1为%f米,组2为%f米\n", 107 | precision, DIST_PER_DEGREE*precision, math.Sqrt(variance/TEST_DATA_NUM), math.Sqrt(variance2/TEST_DATA_NUM)) 108 | return 109 | } 110 | -------------------------------------------------------------------------------- /types/indexed_document_gen.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | // NOTE: THIS FILE WAS PRODUCED BY THE 4 | // MSGP CODE GENERATION TOOL (github.com/tinylib/msgp) 5 | // DO NOT EDIT 6 | 7 | import ( 8 | "github.com/tinylib/msgp/msgp" 9 | ) 10 | 11 | // DecodeMsg implements msgp.Decodable 12 | func (z *IndexedDocument) DecodeMsg(dc *msgp.Reader) (err error) { 13 | var field []byte 14 | _ = field 15 | var zxvk uint32 16 | zxvk, err = dc.ReadMapHeader() 17 | if err != nil { 18 | return 19 | } 20 | for zxvk > 0 { 21 | zxvk-- 22 | field, err = dc.ReadMapKeyPtr() 23 | if err != nil { 24 | return 25 | } 26 | switch msgp.UnsafeString(field) { 27 | case "id": 28 | z.DocId, err = dc.ReadUint64() 29 | if err != nil { 30 | return 31 | } 32 | case "lat": 33 | z.Latitude, err = dc.ReadFloat64() 34 | if err != nil { 35 | return 36 | } 37 | case "long": 38 | z.Longitude, err = dc.ReadFloat64() 39 | if err != nil { 40 | return 41 | } 42 | case "f": 43 | z.Fields, err = dc.ReadIntf() 44 | if err != nil { 45 | return 46 | } 47 | default: 48 | err = dc.Skip() 49 | if err != nil { 50 | return 51 | } 52 | } 53 | } 54 | return 55 | } 56 | 57 | // EncodeMsg implements msgp.Encodable 58 | func (z *IndexedDocument) EncodeMsg(en *msgp.Writer) (err error) { 59 | // map header, size 4 60 | // write "id" 61 | err = en.Append(0x84, 0xa2, 0x69, 0x64) 62 | if err != nil { 63 | return err 64 | } 65 | err = en.WriteUint64(z.DocId) 66 | if err != nil { 67 | return 68 | } 69 | // write "lat" 70 | err = en.Append(0xa3, 0x6c, 0x61, 0x74) 71 | if err != nil { 72 | return err 73 | } 74 | err = en.WriteFloat64(z.Latitude) 75 | if err != nil { 76 | return 77 | } 78 | // write "long" 79 | err = en.Append(0xa4, 0x6c, 0x6f, 0x6e, 0x67) 80 | if err != nil { 81 | return err 82 | } 83 | err = en.WriteFloat64(z.Longitude) 84 | if err != nil { 85 | return 86 | } 87 | // write "f" 88 | err = en.Append(0xa1, 0x66) 89 | if err != nil { 90 | return err 91 | } 92 | err = en.WriteIntf(z.Fields) 93 | if err != nil { 94 | return 95 | } 96 | return 97 | } 98 | 99 | // MarshalMsg implements msgp.Marshaler 100 | func (z *IndexedDocument) MarshalMsg(b []byte) (o []byte, err error) { 101 | o = msgp.Require(b, z.Msgsize()) 102 | // map header, size 4 103 | // string "id" 104 | o = append(o, 0x84, 0xa2, 0x69, 0x64) 105 | o = msgp.AppendUint64(o, z.DocId) 106 | // string "lat" 107 | o = append(o, 0xa3, 0x6c, 0x61, 0x74) 108 | o = msgp.AppendFloat64(o, z.Latitude) 109 | // string "long" 110 | o = append(o, 0xa4, 0x6c, 0x6f, 0x6e, 0x67) 111 | o = msgp.AppendFloat64(o, z.Longitude) 112 | // string "f" 113 | o = append(o, 0xa1, 0x66) 114 | o, err = msgp.AppendIntf(o, z.Fields) 115 | if err != nil { 116 | return 117 | } 118 | return 119 | } 120 | 121 | // UnmarshalMsg implements msgp.Unmarshaler 122 | func (z *IndexedDocument) UnmarshalMsg(bts []byte) (o []byte, err error) { 123 | var field []byte 124 | _ = field 125 | var zbzg uint32 126 | zbzg, bts, err = msgp.ReadMapHeaderBytes(bts) 127 | if err != nil { 128 | return 129 | } 130 | for zbzg > 0 { 131 | zbzg-- 132 | field, bts, err = msgp.ReadMapKeyZC(bts) 133 | if err != nil { 134 | return 135 | } 136 | switch msgp.UnsafeString(field) { 137 | case "id": 138 | z.DocId, bts, err = msgp.ReadUint64Bytes(bts) 139 | if err != nil { 140 | return 141 | } 142 | case "lat": 143 | z.Latitude, bts, err = msgp.ReadFloat64Bytes(bts) 144 | if err != nil { 145 | return 146 | } 147 | case "long": 148 | z.Longitude, bts, err = msgp.ReadFloat64Bytes(bts) 149 | if err != nil { 150 | return 151 | } 152 | case "f": 153 | z.Fields, bts, err = msgp.ReadIntfBytes(bts) 154 | if err != nil { 155 | return 156 | } 157 | default: 158 | bts, err = msgp.Skip(bts) 159 | if err != nil { 160 | return 161 | } 162 | } 163 | } 164 | o = bts 165 | return 166 | } 167 | 168 | // Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message 169 | func (z *IndexedDocument) Msgsize() (s int) { 170 | s = 1 + 3 + msgp.Uint64Size + 4 + msgp.Float64Size + 5 + msgp.Float64Size + 2 + msgp.GuessSize(z.Fields) 171 | return 172 | } 173 | -------------------------------------------------------------------------------- /example/view/build/webpack.prod.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const path = require('path') 3 | const utils = require('./utils') 4 | const webpack = require('webpack') 5 | const config = require('../config') 6 | const merge = require('webpack-merge') 7 | const baseWebpackConfig = require('./webpack.base.conf') 8 | const CopyWebpackPlugin = require('copy-webpack-plugin') 9 | const HtmlWebpackPlugin = require('html-webpack-plugin') 10 | const ExtractTextPlugin = require('extract-text-webpack-plugin') 11 | const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin') 12 | 13 | const env = config.build.env 14 | 15 | const webpackConfig = merge(baseWebpackConfig, { 16 | module: { 17 | rules: utils.styleLoaders({ 18 | sourceMap: config.build.productionSourceMap, 19 | extract: true 20 | }) 21 | }, 22 | devtool: config.build.productionSourceMap ? '#source-map' : false, 23 | output: { 24 | path: config.build.assetsRoot, 25 | filename: utils.assetsPath('js/[name].[chunkhash].js'), 26 | chunkFilename: utils.assetsPath('js/[id].[chunkhash].js') 27 | }, 28 | plugins: [ 29 | // http://vuejs.github.io/vue-loader/en/workflow/production.html 30 | new webpack.DefinePlugin({ 31 | 'process.env': env 32 | }), 33 | // UglifyJs do not support ES6+, you can also use babel-minify for better treeshaking: https://github.com/babel/minify 34 | new webpack.optimize.UglifyJsPlugin({ 35 | compress: { 36 | warnings: false 37 | }, 38 | sourceMap: true 39 | }), 40 | // extract css into its own file 41 | new ExtractTextPlugin({ 42 | filename: utils.assetsPath('css/[name].[contenthash].css') 43 | }), 44 | // Compress extracted CSS. We are using this plugin so that possible 45 | // duplicated CSS from different components can be deduped. 46 | new OptimizeCSSPlugin({ 47 | cssProcessorOptions: { 48 | safe: true 49 | } 50 | }), 51 | // generate dist index.html with correct asset hash for caching. 52 | // you can customize output by editing /index.html 53 | // see https://github.com/ampedandwired/html-webpack-plugin 54 | new HtmlWebpackPlugin({ 55 | filename: config.build.index, 56 | template: 'index.html', 57 | inject: true, 58 | minify: { 59 | removeComments: true, 60 | collapseWhitespace: true, 61 | removeAttributeQuotes: true 62 | // more options: 63 | // https://github.com/kangax/html-minifier#options-quick-reference 64 | }, 65 | // necessary to consistently work with multiple chunks via CommonsChunkPlugin 66 | chunksSortMode: 'dependency' 67 | }), 68 | // keep module.id stable when vender modules does not change 69 | new webpack.HashedModuleIdsPlugin(), 70 | // split vendor js into its own file 71 | new webpack.optimize.CommonsChunkPlugin({ 72 | name: 'vendor', 73 | minChunks: function (module) { 74 | // any required modules inside node_modules are extracted to vendor 75 | return ( 76 | module.resource && 77 | /\.js$/.test(module.resource) && 78 | module.resource.indexOf( 79 | path.join(__dirname, '../node_modules') 80 | ) === 0 81 | ) 82 | } 83 | }), 84 | // extract webpack runtime and module manifest to its own file in order to 85 | // prevent vendor hash from being updated whenever app bundle is updated 86 | new webpack.optimize.CommonsChunkPlugin({ 87 | name: 'manifest', 88 | chunks: ['vendor'] 89 | }), 90 | // copy custom static assets 91 | new CopyWebpackPlugin([ 92 | { 93 | from: path.resolve(__dirname, '../static'), 94 | to: config.build.assetsSubDirectory, 95 | ignore: ['.*'] 96 | } 97 | ]) 98 | ] 99 | }) 100 | 101 | if (config.build.productionGzip) { 102 | const CompressionWebpackPlugin = require('compression-webpack-plugin') 103 | 104 | webpackConfig.plugins.push( 105 | new CompressionWebpackPlugin({ 106 | asset: '[path].gz[query]', 107 | algorithm: 'gzip', 108 | test: new RegExp( 109 | '\\.(' + 110 | config.build.productionGzipExtensions.join('|') + 111 | ')$' 112 | ), 113 | threshold: 10240, 114 | minRatio: 0.8 115 | }) 116 | ) 117 | } 118 | 119 | if (config.build.bundleAnalyzerReport) { 120 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin 121 | webpackConfig.plugins.push(new BundleAnalyzerPlugin()) 122 | } 123 | 124 | module.exports = webpackConfig 125 | -------------------------------------------------------------------------------- /distanceMeasure/distanceMeasure.go: -------------------------------------------------------------------------------- 1 | package distanceMeasure 2 | 3 | /* 4 | 使用本package的DistanceMeasure结构体建议使用GetInstance和CreateNewMeasure来获取对象, 5 | 而不建议自己直接new。因为如果自己new可能会在切换城市的时候忘记设置本地的location 6 | GetInstance和CreateNewMeasure的区别是: 7 | 1.如果一个进程同时只为一个城市或一个区(比如上海这么人口密集的城市可以一个区一个基准坐标,这样误差也会小)服务, 8 | 那么建议使用GetInstance通常只需要在启动时以读入配置文件的配置项的方式来设置就行了。 9 | 2.如果一个进程同时为多个城市或者多个区服务,那么建议使用CreateNewMeasure,这样调用SetLocalEarthCoordinate 10 | 设置本地城市坐标时才不会互相覆盖和影响 11 | MeasureByStardardMethod的数学推导:http://blog.csdn.net/liminlu0314/article/details/8553926 12 | MeasureByQuickMethodWithoutLocation的参考链接:https://tech.meituan.com/lucene-distance.html 13 | */ 14 | 15 | import ( 16 | "fmt" 17 | "math" 18 | ) 19 | 20 | const RADIUS = 6378137.0 //地球半径,单位米 21 | const DIST_PER_DEGREE = math.Pi * 35433.88889 // πr/180° 解释为每度所表示的实际长度 22 | 23 | var localMeasure *DistanceMeasure = nil 24 | 25 | type DistanceMeasure struct { 26 | IsSetLocation bool //是否设置了本地基准经纬度 27 | Benchmark *EarthCoordinate //基准坐标 28 | cosLatitude float64 //math.Cos(Benchmark.latitude) 基准维度的cos值 29 | cityName string //城市名,可以不填,打印使用的 30 | IsFirstUse bool //是否是第一次使用,在GetInstance使用后置为ture 31 | } 32 | 33 | type MeasureError struct { 34 | } 35 | 36 | func (e MeasureError) Error() string { 37 | return "尚未输入本地城市的基准经纬度坐标" 38 | } 39 | 40 | type EarthCoordinate struct { 41 | Longitude float64 //经度坐标 东经为正数,西经为负数 42 | Latitude float64 //纬度坐标 北纬为正数,南纬为负数 43 | } 44 | 45 | //返回单例 46 | func GetInstance() *DistanceMeasure { 47 | if localMeasure == nil { 48 | localMeasure = new(DistanceMeasure) 49 | localMeasure.IsSetLocation = false 50 | localMeasure.Benchmark = &EarthCoordinate{0.0, 0.0} 51 | } 52 | localMeasure.IsFirstUse = true 53 | return localMeasure 54 | } 55 | 56 | //创建一个新的DistanceMeasure对象 57 | func CreateNewMeasure() *DistanceMeasure{ 58 | measure := new(DistanceMeasure) 59 | measure.IsSetLocation = false 60 | measure.Benchmark = &EarthCoordinate{0.0, 0.0} 61 | measure.IsFirstUse = true 62 | return measure 63 | } 64 | 65 | //设置本地的基准坐标,可以取一个城市的市中心,EarthCoordinate必须要输入所在城市的经纬度坐标,name值(城市名)可以为空 66 | func (this *DistanceMeasure) SetLocalEarthCoordinate(location *EarthCoordinate, name string) { 67 | this.IsSetLocation = true //已设置坐标 68 | this.Benchmark = location //用于QuickMethod的本地经纬度坐标 69 | this.cityName = name //城市名,打印使用 70 | this.cosLatitude = math.Cos(this.ChangeAngleToRadian(location.Latitude)) //基准维度的cos值 71 | fmt.Println("设置了城市坐标,城市名:", name, "经纬度坐标:", location) 72 | } 73 | 74 | func (this *DistanceMeasure) ChangeAngleToRadian(angle float64) float64 { 75 | return angle / 180.0 * math.Pi 76 | } 77 | 78 | //标准球体测距算法,基准算法,基于球面模型来处理的(立体几何),即Haversine公式 79 | func (this *DistanceMeasure) MeasureByStardardMethod(pt1, pt2 *EarthCoordinate) float64 { 80 | //先把角度转成弧度 81 | lon1 := this.ChangeAngleToRadian(pt1.Longitude) 82 | lat1 := this.ChangeAngleToRadian(pt1.Latitude) 83 | lon2 := this.ChangeAngleToRadian(pt2.Longitude) 84 | lat2 := this.ChangeAngleToRadian(pt2.Latitude) 85 | //因为cos余弦函数是偶函数,所以lon2-lon1还是lon1-lon2都一样 86 | //得到距离的弧度值 87 | radDist := math.Acos(math.Sin(lat1)*math.Sin(lat2) + math.Cos(lat1)*math.Cos(lat2)*math.Cos(lon2-lon1)) 88 | return radDist * RADIUS //乘以地球半径,返回实际长度 89 | } 90 | 91 | //未提前设置本地城市经纬度坐标的快速测距算法 92 | func (this *DistanceMeasure) MeasureByQuickMethodWithoutLocation(pt1, pt2 *EarthCoordinate) float64 { 93 | diffLat := (pt1.Latitude - pt2.Latitude) //纬度差的实际距离,单位m 94 | radLat := this.ChangeAngleToRadian((pt1.Latitude + pt2.Latitude) / 2) 95 | diffLog := math.Cos(radLat) * (pt1.Longitude - pt2.Longitude) //经度差的实际距离,单位m,需要乘以cos(所在纬度),因为纬度越高时经度差的表示的实际距离越短 96 | //根据勾股定理 97 | dist := DIST_PER_DEGREE * math.Sqrt(diffLat*diffLat+diffLog*diffLog) 98 | return dist 99 | } 100 | 101 | //已提前设置本地城市经纬度坐标的快速测距算法 102 | func (this *DistanceMeasure) MeasureByQuickMethod(pt1, pt2 *EarthCoordinate) (float64, error) { 103 | //先判断是否已经输入了本地基准经纬度坐标 104 | 105 | if this.IsSetLocation == false { 106 | return 0.0, MeasureError{} 107 | } 108 | if this.IsFirstUse { 109 | this.IsFirstUse = false 110 | //第一次调用这个函数的时候打印城市名进行提示 111 | var strLon, strLat string 112 | if this.Benchmark.Longitude >= 0 { 113 | strLon = fmt.Sprintf("东经%f°", this.Benchmark.Longitude) 114 | } else { 115 | strLon = fmt.Sprintf("西经%f°", -this.Benchmark.Longitude) 116 | } 117 | if this.Benchmark.Latitude >= 0 { 118 | strLat = fmt.Sprintf("北纬%f°", this.Benchmark.Latitude) 119 | } else { 120 | strLat = fmt.Sprintf("南纬%f°", -this.Benchmark.Latitude) 121 | } 122 | fmt.Printf("本次快速测距算法设置的城市为%s,输入的基准经纬度坐标为[%s, %s]\n", this.cityName, strLon, strLat) 123 | fmt.Println("如果当前保存的城市与你期望的城市不符,请重新调用SetLocalEarthCoordinate来设置") 124 | } 125 | 126 | diffLat := (pt1.Latitude - pt2.Latitude) //纬度差的实际距离,单位m 127 | diffLog := this.cosLatitude * (pt1.Longitude - pt2.Longitude) //经度差的实际距离,单位m,需要乘以cos(所在纬度),因为纬度越高时经度差的表示的实际距离越短 128 | distance := DIST_PER_DEGREE * math.Sqrt(diffLat*diffLat+diffLog*diffLog) 129 | return distance, nil 130 | } 131 | -------------------------------------------------------------------------------- /core/indexer.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "github.com/go-redis/redis" 5 | "github.com/mmcloughlin/geohash" 6 | "github.com/sillydong/lbsengine/distanceMeasure" 7 | "github.com/sillydong/lbsengine/types" 8 | "log" 9 | "strconv" 10 | "unsafe" 11 | ) 12 | 13 | type Indexer struct { 14 | option *types.IndexerOptions 15 | client *redis.Client 16 | measure *distanceMeasure.DistanceMeasure 17 | } 18 | 19 | func (i *Indexer) Init(option *types.IndexerOptions) { 20 | i.option = option 21 | i.client = redis.NewClient(&redis.Options{ 22 | Addr: option.RedisHost, 23 | DB: option.RedisDb, 24 | Password: option.RedisPassword, 25 | }) 26 | err := i.client.Ping().Err() 27 | if err != nil { 28 | log.Fatal(err) 29 | } 30 | //距离计算 31 | i.measure = distanceMeasure.GetInstance() 32 | if option.CenterLongitude != 0.0 && option.CenterLatitude != 0.0 { 33 | i.measure.SetLocalEarthCoordinate(&distanceMeasure.EarthCoordinate{Latitude: option.CenterLatitude, Longitude: option.CenterLongitude}, option.Location) 34 | } 35 | } 36 | 37 | func (i *Indexer) Add(doc *types.IndexedDocument) { 38 | sdocid := strconv.FormatUint(doc.DocId, 10) 39 | hashs := i.hashshard(doc.DocId) 40 | pip := i.client.Pipeline() 41 | //删除旧数据 42 | oldhash, _ := i.client.HGet(hashs, sdocid).Result() 43 | if len(oldhash) > 0 { 44 | pip.HDel(i.geoshard(oldhash, doc.DocId), sdocid) 45 | } 46 | newhash := geohash.EncodeWithPrecision(doc.Latitude, doc.Longitude, i.option.GeoPrecious) 47 | pip.HSet(hashs, sdocid, newhash) 48 | pip.HSet(i.geoshard(newhash, doc.DocId), sdocid, doc) 49 | _, err := pip.Exec() 50 | if err != nil { 51 | log.Print(err) 52 | } 53 | } 54 | 55 | func (i *Indexer) Remove(docid uint64) { 56 | sdocid := strconv.FormatUint(docid, 10) 57 | hashs := i.hashshard(docid) 58 | hash, _ := i.client.HGet(hashs, sdocid).Result() 59 | if len(hash) > 0 { 60 | geos := i.geoshard(hash, docid) 61 | pip := i.client.Pipeline() 62 | pip.HDel(hashs, sdocid) 63 | pip.HDel(geos, sdocid) 64 | _, err := pip.Exec() 65 | if err != nil { 66 | log.Print(err) 67 | } 68 | } 69 | } 70 | 71 | func (i *Indexer) Search(countonly bool, hash string, latitude, longitude float64, options *types.SearchOptions) (docs types.ScoredDocuments, count int) { 72 | strs := i.client.HVals(hash).Val() 73 | if len(strs) > 0 { 74 | if countonly { 75 | //仅计算数量 76 | if options.Excepts == nil && options.Filter == nil { 77 | count += len(strs) 78 | } else { 79 | for _, str := range strs { 80 | document := types.IndexedDocument{} 81 | document.UnmarshalMsg(tobytes(str)) 82 | if document.DocId != 0 { 83 | //判断是否排除 84 | if options.Excepts == nil { 85 | //判断是否过滤 86 | if options.Filter == nil || options.Filter(document) { 87 | count++ 88 | } 89 | } else { 90 | if _, ok := options.Excepts[document.DocId]; !ok { 91 | if options.Filter == nil || options.Filter(document) { 92 | count++ 93 | } 94 | } 95 | } 96 | } 97 | } 98 | } 99 | } else { 100 | //需要数据 101 | for _, str := range strs { 102 | document := types.IndexedDocument{} 103 | document.UnmarshalMsg(tobytes(str)) 104 | if document.DocId != 0 { 105 | //判断是否排除 106 | if options.Excepts == nil { 107 | //判断是否过滤 108 | if options.Filter == nil || options.Filter(document) { 109 | doc := types.ScoredDocument{ 110 | DocId: document.DocId, 111 | Distance: i.distance(options.Accuracy, document.Latitude, document.Longitude, latitude, longitude), 112 | } 113 | docs = append(docs, &doc) 114 | count++ 115 | } 116 | } else { 117 | if _, ok := options.Excepts[document.DocId]; !ok { 118 | if options.Filter == nil || options.Filter(document) { 119 | doc := types.ScoredDocument{ 120 | DocId: document.DocId, 121 | Distance: i.distance(options.Accuracy, document.Latitude, document.Longitude, latitude, longitude), 122 | } 123 | docs = append(docs, &doc) 124 | count++ 125 | } 126 | } 127 | } 128 | } 129 | } 130 | } 131 | } 132 | return 133 | } 134 | 135 | //距离计算 136 | func (i *Indexer) distance(accuracy int, alatitude, alongitude, blatitude, blongitude float64) float64 { 137 | //fmt.Printf("%+v - %+v ------ %+v - %+v",alatitude,alongitude,blatitude,blongitude) 138 | a := &distanceMeasure.EarthCoordinate{Latitude: alatitude, Longitude: alongitude} 139 | b := &distanceMeasure.EarthCoordinate{Latitude: blatitude, Longitude: blongitude} 140 | switch accuracy { 141 | case types.STANDARD: 142 | return i.measure.MeasureByStardardMethod(a, b) 143 | case types.MEITUAN: 144 | return i.measure.MeasureByQuickMethodWithoutLocation(a, b) 145 | case types.IMPROVED: 146 | distance, err := i.measure.MeasureByQuickMethod(a, b) 147 | if err != nil { 148 | return i.measure.MeasureByQuickMethodWithoutLocation(a, b) 149 | } 150 | return distance 151 | } 152 | return 0.0 153 | } 154 | 155 | func (i *Indexer) hashshard(docid uint64) string { 156 | return "g_" + strconv.FormatUint((uint64(docid/i.option.HashSize)+1)*i.option.HashSize, 10) 157 | } 158 | 159 | func (i *Indexer) geoshard(hash string, docid uint64) string { 160 | return "h_" + hash + "_" + strconv.FormatUint(docid-docid/i.option.GeoShard*i.option.GeoShard, 10) 161 | } 162 | 163 | func tobytes(s string) []byte { 164 | x := (*[2]uintptr)(unsafe.Pointer(&s)) 165 | h := [3]uintptr{x[0], x[1], x[1]} 166 | return *(*[]byte)(unsafe.Pointer(&h)) 167 | } 168 | -------------------------------------------------------------------------------- /engine/engine.go: -------------------------------------------------------------------------------- 1 | package engine 2 | 3 | import ( 4 | "fmt" 5 | "github.com/huichen/murmur" 6 | "github.com/sillydong/lbsengine/core" 7 | "github.com/sillydong/lbsengine/types" 8 | "sort" 9 | "strconv" 10 | "time" 11 | "unsafe" 12 | ) 13 | 14 | type Engine struct { 15 | option *types.EngineOptions 16 | 17 | cacher *core.Cacher 18 | indexer *core.Indexer 19 | 20 | indexerAddChannels []chan *types.IndexedDocument 21 | indexerRemoveChannels []chan uint64 22 | indexerSearchChannels []chan *indexerSearchRequest 23 | } 24 | 25 | func (e *Engine) Init(option *types.EngineOptions) { 26 | if option == nil { 27 | option = &types.EngineOptions{} 28 | } 29 | option.Init() 30 | e.option = option 31 | 32 | //初始化缓存器 33 | e.cacher = &core.Cacher{} 34 | e.cacher.Init() 35 | //初始化索引器 36 | e.indexer = &core.Indexer{} 37 | e.indexer.Init(e.option.IndexerOption) 38 | 39 | e.indexerAddChannels = make([]chan *types.IndexedDocument, e.option.NumShards) 40 | e.indexerRemoveChannels = make([]chan uint64, e.option.NumShards) 41 | e.indexerSearchChannels = make([]chan *indexerSearchRequest, e.option.NumShards) 42 | 43 | for i := 0; i < int(e.option.NumShards); i++ { 44 | //初始化channel 45 | e.indexerAddChannels[i] = make(chan *types.IndexedDocument, e.option.AddBuffer) 46 | e.indexerRemoveChannels[i] = make(chan uint64, e.option.RemoveBuffer) 47 | e.indexerSearchChannels[i] = make(chan *indexerSearchRequest, e.option.SearchBuffer) 48 | 49 | go e.indexerAddWorker(i) 50 | go e.indexerRemoveWorker(i) 51 | for k := 0; k < e.option.SearchWorkerThreads; k++ { 52 | go e.indexerSearchWorker(i) 53 | } 54 | } 55 | } 56 | 57 | func (e *Engine) Add(doc *types.IndexedDocument) { 58 | shard := e.shardid(doc.DocId) 59 | e.indexerAddChannels[shard] <- doc 60 | } 61 | 62 | func (e *Engine) Remove(docid uint64) { 63 | shard := e.shardid(docid) 64 | e.indexerRemoveChannels[shard] <- docid 65 | } 66 | 67 | func (e *Engine) Search(request *types.SearchRequest) *types.SearchResponse { 68 | content := fmt.Sprintf("%v", request) 69 | hash := murmur.Murmur3(tobytes(content)) 70 | 71 | shard := int(hash - hash/uint32(e.option.NumShards)*uint32(e.option.NumShards)) 72 | 73 | cachekey := fmt.Sprintf("%v", hash) 74 | 75 | if request.SearchOption == nil { 76 | request.SearchOption = e.option.DefaultSearchOption 77 | } 78 | request.SearchOption.Init() 79 | 80 | //是否刷新缓存 81 | if !request.SearchOption.Refresh { 82 | //从缓存取数据 83 | docs, count := e.cacher.Get(cachekey, request.Offset, request.Limit) 84 | if docs != nil && count > 0 { 85 | return &types.SearchResponse{Docs: docs, Count: count, Timeout: false} 86 | } 87 | } 88 | 89 | //获取neighbour 90 | neighbours := core.LoopNeighbours(request.Latitude, request.Longitude, e.option.IndexerOption.GeoPrecious, request.SearchOption.Circles) 91 | loops := len(neighbours) * int(e.option.IndexerOption.GeoShard) //geohash分片 92 | indexerReturnChannel := make(chan *indexerSearchResponse, loops) 93 | 94 | //下发任务 95 | for _, geo := range neighbours { 96 | for i := 0; i < int(e.option.IndexerOption.GeoShard); i++ { 97 | geoshard := "h_" + geo + "_" + strconv.Itoa(i) 98 | e.indexerSearchChannels[shard] <- &indexerSearchRequest{ 99 | countonly: request.CountOnly, 100 | hash: geoshard, 101 | latitude: request.Latitude, 102 | longitude: request.Longitude, 103 | option: request.SearchOption, 104 | indexerReturnChannel: indexerReturnChannel, 105 | } 106 | } 107 | } 108 | 109 | //整理数据 110 | docs := types.ScoredDocuments{} 111 | count := 0 112 | istimeout := false 113 | if request.SearchOption.Timeout > 0 { 114 | deadline := time.Now().Add(request.SearchOption.Timeout) 115 | for i := 0; i < loops; i++ { 116 | select { 117 | case lresponse := <-indexerReturnChannel: 118 | if lresponse.count == 0 { 119 | continue 120 | } 121 | if !request.CountOnly { 122 | docs = append(docs, lresponse.docs...) 123 | } 124 | count += lresponse.count 125 | case <-time.After(deadline.Sub(time.Now())): 126 | istimeout = true 127 | break 128 | } 129 | 130 | } 131 | } else { 132 | for i := 0; i < loops; i++ { 133 | lresponse := <-indexerReturnChannel 134 | if lresponse.count == 0 { 135 | continue 136 | } 137 | if !request.CountOnly { 138 | docs = append(docs, lresponse.docs...) 139 | } 140 | count += lresponse.count 141 | } 142 | } 143 | 144 | //最终排序 145 | if !request.CountOnly { 146 | if request.SearchOption.OrderDesc { 147 | sort.Sort(sort.Reverse(docs)) 148 | } else { 149 | sort.Sort(docs) 150 | } 151 | } 152 | 153 | //写缓存 154 | if len(docs) > 0 { 155 | e.cacher.Set(cachekey, docs) 156 | } 157 | 158 | //拼返回数据 159 | response := &types.SearchResponse{} 160 | response.Count = count 161 | if !request.CountOnly { 162 | start := request.Offset 163 | stop := request.Offset + request.Limit 164 | if start > count { 165 | response.Docs = nil 166 | } else if stop > count { 167 | response.Docs = docs[start:] 168 | } else { 169 | response.Docs = docs[start:stop] 170 | } 171 | } else { 172 | response.Docs = nil 173 | } 174 | response.Timeout = istimeout 175 | 176 | return response 177 | 178 | } 179 | 180 | func (e *Engine) shardid(docid uint64) int { 181 | content := fmt.Sprintf("%d", docid) 182 | hash := murmur.Murmur3(tobytes(content)) 183 | return int(hash - hash/uint32(e.option.NumShards)*uint32(e.option.NumShards)) 184 | } 185 | 186 | func tobytes(s string) []byte { 187 | x := (*[2]uintptr)(unsafe.Pointer(&s)) 188 | h := [3]uintptr{x[0], x[1], x[1]} 189 | return *(*[]byte)(unsafe.Pointer(&h)) 190 | } 191 | -------------------------------------------------------------------------------- /example/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "database/sql" 5 | "github.com/Code-Hex/echo-static" 6 | "github.com/elazarl/go-bindata-assetfs" 7 | "github.com/go-gorp/gorp" 8 | "github.com/go-sql-driver/mysql" 9 | "github.com/labstack/echo" 10 | "github.com/labstack/echo/middleware" 11 | "github.com/sillydong/lbsengine/engine" 12 | "github.com/sillydong/lbsengine/types" 13 | "log" 14 | "net/http" 15 | "strconv" 16 | "strings" 17 | "time" 18 | ) 19 | 20 | func main() { 21 | //tidb 22 | dbconfig := &mysql.Config{ 23 | User: "root", 24 | Passwd: "", 25 | DBName: "lbsexample", 26 | Net: "tcp", 27 | Addr: "127.0.0.1:4000", 28 | } 29 | db, err := sql.Open("mysql", dbconfig.FormatDSN()) 30 | if err != nil { 31 | log.Fatal(err) 32 | } 33 | if err := db.Ping(); err != nil { 34 | log.Fatal(err) 35 | } 36 | db.SetConnMaxLifetime(time.Minute) 37 | db.SetMaxIdleConns(10) 38 | db.SetMaxOpenConns(10) 39 | 40 | client := &gorp.DbMap{Db: db, Dialect: gorp.MySQLDialect{}} 41 | 42 | client.AddTableWithName(PoiData{}, "poi_data").SetKeys(true, "id") 43 | 44 | //engine 45 | eg := &engine.Engine{} 46 | eg.Init(nil) 47 | 48 | //echo 49 | addr := ":8877" 50 | e := echo.New() 51 | e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{ 52 | Skipper: ResourceSkipper, 53 | })) 54 | e.Use(middleware.Recover()) 55 | 56 | e.Use(static.ServeRoot("/", &assetfs.AssetFS{ 57 | Asset: Asset, 58 | AssetDir: AssetDir, 59 | AssetInfo: AssetInfo, 60 | Prefix: "view/dist", 61 | })) 62 | api := e.Group("/api") 63 | api.POST("/add", func(context echo.Context) error { 64 | //解析对象 65 | item := new(PoiData) 66 | if err := context.Bind(item); err != nil { 67 | return context.JSON(http.StatusInternalServerError, Response{ 68 | Status: 0, 69 | Error: "请求的参数不正确", 70 | }) 71 | } 72 | 73 | //写tidb 74 | err := client.Insert(item) 75 | if err != nil { 76 | context.Logger().Error(err) 77 | return context.JSON(http.StatusInternalServerError, Response{ 78 | Status: 0, 79 | Error: err.Error(), 80 | }) 81 | } 82 | if item.Id > 0 { 83 | //写engine 84 | eg.Add(&types.IndexedDocument{ 85 | DocId: uint64(item.Id), 86 | Latitude: item.Latitude, 87 | Longitude: item.Longitude, 88 | Fields: nil, 89 | }) 90 | return context.JSON(http.StatusOK, Response{ 91 | Status: 1, 92 | Data: "添加成功", 93 | }) 94 | } else { 95 | return context.JSON(http.StatusInternalServerError, Response{ 96 | Status: 0, 97 | Error: "数据库写入失败", 98 | }) 99 | } 100 | 101 | }) 102 | api.DELETE("/del/:id", func(context echo.Context) error { 103 | id := context.Param("id") 104 | fid, _ := strconv.Atoi(id) 105 | //删tidb 106 | _, err := client.Delete(&PoiData{Id: int64(fid)}) 107 | if err != nil { 108 | context.Logger().Error(err) 109 | } 110 | 111 | //删engine 112 | eg.Remove(uint64(fid)) 113 | 114 | return context.JSON(http.StatusOK, Response{ 115 | Status: 1, 116 | Data: "删除成功", 117 | }) 118 | }) 119 | api.GET("/query", func(context echo.Context) error { 120 | latitude := context.QueryParam("latitude") 121 | longitude := context.QueryParam("longitude") 122 | offset := context.QueryParam("offset") 123 | 124 | flat, _ := strconv.ParseFloat(latitude, 10) 125 | flng, _ := strconv.ParseFloat(longitude, 10) 126 | foff, _ := strconv.Atoi(offset) 127 | 128 | //查engine 129 | resp := eg.Search(&types.SearchRequest{ 130 | Latitude: flat, 131 | Longitude: flng, 132 | CountOnly: false, 133 | Offset: foff, 134 | Limit: 10, 135 | }) 136 | if resp.Count > 0 && resp.Docs != nil { 137 | //拼数据 138 | ids := make([]interface{}, len(resp.Docs)) 139 | for i, doc := range resp.Docs { 140 | ids[i] = doc.DocId 141 | //context.Logger().Printf("%+v\n",doc) 142 | } 143 | var datas []PoiData 144 | _, err := client.Select(&datas, "select * from poi_data where id in (?"+strings.Repeat(",?", len(resp.Docs)-1)+")", ids...) 145 | if err != nil { 146 | context.Logger().Error(err) 147 | } 148 | 149 | datasmap := make(map[uint64]PoiData, len(datas)) 150 | for _, data := range datas { 151 | datasmap[uint64(data.Id)] = data 152 | } 153 | for _, doc := range resp.Docs { 154 | doc.Model = datasmap[doc.DocId] 155 | } 156 | 157 | return context.JSON(http.StatusOK, Response{ 158 | Status: 1, 159 | Data: resp, 160 | }) 161 | } else { 162 | return context.JSON(http.StatusOK, Response{ 163 | Status: 0, 164 | Error: "无结果", 165 | }) 166 | } 167 | }) 168 | api.GET("/init", func(context echo.Context) error { 169 | var datas []PoiData 170 | _, err := client.Select(&datas, "select * from poi_data") 171 | if err != nil { 172 | context.Logger().Error(err) 173 | } 174 | 175 | context.Logger().Printf("about to import %v lines", len(datas)) 176 | 177 | for _, item := range datas { 178 | context.Logger().Print(item.Id) 179 | eg.Add(&types.IndexedDocument{ 180 | DocId: uint64(item.Id), 181 | Latitude: item.Latitude, 182 | Longitude: item.Longitude, 183 | Fields: nil, 184 | }) 185 | } 186 | 187 | return context.JSON(http.StatusOK, "导入完成") 188 | }) 189 | e.Logger.Fatal(e.Start(addr)) 190 | } 191 | 192 | func ResourceSkipper(c echo.Context) bool { 193 | if strings.HasPrefix(c.Path(), "/static") { 194 | return true 195 | } 196 | return false 197 | } 198 | 199 | type PoiData struct { 200 | Id int64 `db:"id" form:"-"` 201 | Name string `db:"name" form:"name"` 202 | AmapId string `db:"amapid" form:"amapid"` 203 | Location string `db:"location" form:"location"` 204 | Latitude float64 `db:"latitude" form:"latitude"` 205 | Longitude float64 `db:"longitude" form:"longitude"` 206 | CreateTime string `db:"create_time" form:"create_time"` 207 | } 208 | 209 | type Response struct { 210 | Status int `json:"status"` 211 | Error string `json:"error"` 212 | Data interface{} `json:"data"` 213 | } 214 | --------------------------------------------------------------------------------