[TOC]
## 概況
### 背景
搜索引擎是個好東西,GIS也是個好東西。當前還有Django和Ionic。
### Showcase
最后效果圖

elasticsearch_ionic_map

elasticsearch_ionic_info_page
### 構架設計
對我們的需求進行簡要的思考后,設計出了下面的一些簡單的架構。

Django ElasticSearch Ionic 架構
#### 服務端
簡單說明:
* 用戶在前臺或者后臺創建數據。
* 在model保存數據的時候,會調用Google的API解析GPS
* 在haystack的配置中設置實時更新,當數據創建的時候自動更新索引
* 數據被ElasticSearch索引
下面是框架的一些簡單的介紹
**Django**
> [Django](http://www.phodal.com/blog/tag/django/)?是一個開放源代碼的Web應用框架,由Python寫成。采用了MVC的軟件設計模式,即模型M,視圖V和控制器C。它最初是被開發來用于管理勞倫斯出版集團旗下的一些以新聞內容為主的網站的。并于2005年7月在BSD許可證下發布。這套框架是以比利時的吉普賽爵士吉他手Django Reinhardt來命名的。
> [Django](http://www.phodal.com/blog/tag/django/)?的主要目標是使得開發復雜的、數據庫驅動的網站變得簡單。Django注重組件的重用性和“可插拔性”,敏捷開發和DRY法則(Don’t Repeat Yourself)。在Django中Python被普遍使用,甚至包括配置文件和數據模型。
首先考慮Django,而不是其他Node或者Ruby框架的原因是:
* 內置認證系統
* 內置CSRF
當然這是其他框架也所擁有的,主要特性還有:
* 一個表單序列化及驗證系統,用于HTML表單和適于數據庫存儲的數據之間的轉換。
* 一套協助創建地理信息系統(GIS)的基礎框架
最后一個才是亮點,內置GIS,雖然沒怎么用到,但是至少在部署上還是比較方便的。
**Haystack**
> Haystack provides modular search for Django. It features a unified, familiar API that allows you to plug in different search backends (such as Solr, Elasticsearch, Whoosh, Xapian, etc.) without having to modify your code.
Haystack是為Django提供一個搜索模塊blabla..,他的主要特性是可以
> write your search code once and choose the search engine you want it to run on
也就是說你只需要寫你的代碼選擇你的搜索引擎就可以工作了。
**ElasticSearch**
在上面的Haystack提供了這些一堆的搜索引擎,當然支持地點搜索的只有`Solr`和`ElasticSearch`,他們支持的空間搜索有:
* within
* dwithin
* distance
* order_by(‘distance’)
* polygon
在文檔上沒有寫Solr的polygon搜索,但是實際上也是支持的(詳細見這篇文章:?[google map solr polygon 搜索](http://www.phodal.com/blog/google-map-width-solr-use-polygon-search/%EF%BC%8C%E7%94%A8%E7%9A%84%E5%9C%B0%E5%9B%BE%E6%98%AF%E8%B0%B7%E6%AD%8C%EF%BC%8C%E6%89%80%E4%BB%A5%E9%9C%80%E8%A6%81%E5%85%88%E5%AD%A6%E4%BC%9A%E8%AE%BF%E9%97%AE%E8%B0%B7%E6%AD%8C)。
至于為什么用的是ElasticSearch,是因為之前用Solr做過。。。
#### 客戶端
**簡單說明 —— GET**
1. 當我們訪問Map View的時候,會調用HTML5獲取用戶的位置
2. 根據用戶的位置定位,設置縮放
3. 根據用戶的位置發出ElasticSearch請求,返回結果中帶上距離
4. 顯示
**簡單說明 —— POST**
1. 用戶填寫數據會發給Django API,并驗證
2. 成功時,存入數據庫,更新索引。
**Ionic**
> Ionic提供了一個免費且開源的移動優化HTML,CSS和JS組件庫,來構建高交互性應用。基于Sass構建和AngularJS 優化。
用到的主要是AngularJS,之前用他寫過三個APP。
**Django REST Framework**
與Django Tastypie相比,DRF的主要優勢在于Web界面的調試。
## 步驟
### Step 1: Django GIS 設置
1.創建虛擬環境
~~~
virtualenv -p /usr/bin/python2.67 django-elasticsearch
~~~
2.創建項目
為了方便,這里用的是Mezzanine CMS,相比Django的主要優勢是,以后擴展方便。但是對于Django也是可以的。
3.安裝依賴
這里我的所有依賴有
* django-haystack
* Mezzanine==3.1.10
* djangorestframework
* pygeocoder
* elasticsearch
安裝
~~~
pip install requirements.txt
~~~
4.安裝ElasticSearch
CentOS
~~~
wget https://download.elasticsearch.org/elasticsearch/elasticsearch/elasticsearch-1.4.2.zip
sudo unzip elasticsearch-1.4.2 -d /usr/local/elasticsearch
rm elasticsearch-1.4.2.zip
cd /usr/local/elasticsearch/elasticsearch-1.4.2/
./bin/plugin install elasticsearch/elasticsearch-cloud-aws/2.4.1
curl -XGET http://localhost:9200
~~~
Mac OS
~~~
brew install elasticsearch
~~~
5.Django Geo環境搭建
CentOS等GNU/Linux系統: 可以參照[CentOS Django Geo 環境搭建](http://www.phodal.com/blog/install-geo-django-in-centos/)
MacOS:?[Mac OS Django Geo 環境搭建](http://www.phodal.com/blog/django-elasticsearch-geo-solution/)
### Step 2: 配置Haystack
**配置Haystack**
~~~
HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor'
HAYSTACK_CONNECTIONS = {
'default': {
'ENGINE': 'haystack.backends.elasticsearch_backend.ElasticsearchSearchEngine',
'URL': 'http://127.0.0.1:9200/',
'INDEX_NAME': 'haystack',
},
}
~~~
`HAYSTACK_SIGNAL_PROCESSOR`是為了可以實時處理。?`HAYSTACK_CONNECTIONS`?則是配置搜索引擎用的。
**配置Django**
在`settings.py`中的`INSTALLED_APPS`添加
~~~
"haystack",
"rest_framework",
~~~
接著
~~~
python manage.py createdb
python manage.py migreate
~~~
運行
~~~
python manage.py runserver
~~~
官方有一個簡單的文檔說明空間搜索——?[Spatial Search](http://django-haystack.readthedocs.org/en/latest/spatial.html)
里面只有`Solr`和`ElasticSearch`是支持的,當然我們也不需要這么復雜的特性。
創建Django app名為nx,目錄結構如下
~~~
.
|______init__.py
|____api.py
|____models.py
|____search_indexes.py
|____templates
| |____search
| | |____indexes
| | | |____nx
| | | | |____note_text.txt
~~~
api.py是后面要用的。
### Step 3: Django Haystack Model創建
而一般的model沒有什么區別,除了修改了save方法
~~~
from django.contrib import admin
from django.contrib.gis.geos import Point
from django.core import validators
from django.utils.translation import ugettext_lazy as _
from django.db import models
from pygeocoder import Geocoder
class Note(models.Model):
title = models.CharField("標題", max_length=30, unique=True)
latitude = models.FloatField(blank=True)
longitude = models.FloatField(blank=True)
def __unicode__(self):
return self.title
def save(self, *args, **kwargs):
results = Geocoder.geocode(self.province + self.city + self.address)
self.latitude = results[0].coordinates[0]
self.longitude = results[0].coordinates[1]
super(Note, self).save(*args, **kwargs)
def get_location(self):
return Point(self.longitude, self.latitude)
def get_location_info(self):
return self.province + self.city + self.address
admin.site.register(Note)
~~~
通過`Geocoder.geocode`?解析用戶輸入的地址,為了方便直接后臺管理了。
### Step 4: 創建search_index
在源碼的目錄下有一個`search_indexes.py`的文件就是用于索引用的。
~~~
from haystack import indexes
from .models import Note
class NoteIndex(indexes.SearchIndex, indexes.Indexable):
text = indexes.CharField(document=True, use_template=True)
title = indexes.CharField(model_attr='title')
location = indexes.LocationField(model_attr='get_location')
location_info = indexes.CharField(model_attr='get_location_info')
def get_model(self):
return Note
~~~
與些同時我們還需要在`templates/search/indexes/nx/`目錄中有`note_text.txt`里面的內容是:
~~~
{{ object.title }}
{{ object.get_location }}
{{ object.get_location_info }}
~~~
**創建數據**
migrate數據庫
~~~
python manage.py migrate
~~~
run
~~~
python manage.py runserver
~~~
接著我們就可以后臺創建數據了。 打開: http://127.0.0.1:8000/admin/nx/note/,把除了`Latitude`和`Longitude`以外的數據都一填——經緯度是自動生成的。就可以創建數據了。
**測試**
訪問 http://localhost:9200/haystack/_search
或者
~~~
curl -XGET http://127.0.0.1:9200/haystack/_search
~~~
如果你沒有Ionic的經驗,可以參考一下之前的一些文章:[《HTML5打造原生應用——Ionic框架簡介與Ionic Hello World》](http://www.phodal.com/blog/ionic-development-android-ios-windows-phone-application/)。
我們用到的庫有:
* elasticsearch
* ionic
* ngCordova
將他們添加到`bower.json`,然后執行
~~~
bower install
~~~
### Step 4: Ionic ElasticSearch 創建頁面
1.引入庫
在`index.html`中添加
~~~
<script src="lib/elasticsearch/elasticsearch.angular.min.js"></script>
<script src="lib/ngCordova/dist/ng-cordova.js"></script>
~~~
接著開始寫我們的搜索模板`tab-search.html`
~~~
<ion-view view-title="搜索" ng-controller="SearchCtrl">
<ion-content>
<div id="search-bar">
<div class="item item-input-inset">
<label class="item-input-wrapper" id="search-input">
<i class="icon ion-search placeholder-icon"></i>
<input type="search" placeholder="Search" ng-model="query" ng-change="search(query)" autocorrect="off">
</label>
</div>
</div>
</ion-content>
</ion-view>
~~~
顯示部分
~~~
<ion-list>
<ion-item class="item-remove-animate item-icon-right" ng-repeat="result in results">
<h2 class="icon-left">{{result.title}}</h2>
<p>簡介: {{result.body}}</p>
<div class="icon-left ion-ios-home location_info">
{{result.location_info}}
</div>
<div class="button icon-left ion-ios-telephone button-calm button-outline">
<a ng-href="tel: {{result.phone_number}}">{{result.phone_number}}</a>
</div>
</ion-item>
</ion-list>
~~~
而我們期待的`SearchCtrl`則是這樣的
~~~
$scope.query = "";
var doSearch = ionic.debounce(function(query) {
ESService.search(query, 0).then(function(results){
$scope.results = results;
});
}, 500);
$scope.search = function(query) {
doSearch(query);
}
~~~
當我們點下搜索的時候,調用 ESService.
### Step 5: Ionic ElasticSearch Service
接著我們就來構建我們的ESService,下面的部分來自網上:
~~~
angular.module('starter.services', ['ngCordova', 'elasticsearch'])
.factory('ESService',
['$q', 'esFactory', '$location', '$localstorage', function($q, elasticsearch, $location, $localstorage){
var client = elasticsearch({
host: $location.host() + ":9200"
});
var search = function(term, offset){
var deferred = $q.defer(), query, sort;
if(!term){
query = {
"match_all": {}
};
} else {
query = {
match: { title: term }
}
}
var position = $localstorage.get('position');
if(position){
sort = [{
"_geo_distance": {
"location": position,
"unit": "km"
}
}];
} else {
sort = [];
}
client.search({
"index": 'haystack',
"body": {
"query": query,
"sort": sort
}
}).then(function(result) {
var ii = 0, hits_in, hits_out = [];
hits_in = (result.hits || {}).hits || [];
for(;ii < hits_in.length; ii++){
var data = hits_in[ii]._source;
var distance = {};
if(hits_in[ii].sort){
distance = {"distance": parseFloat(hits_in[ii].sort[0]).toFixed(1)}
}
angular.extend(data, distance);
hits_out.push(data);
}
deferred.resolve(hits_out);
}, deferred.reject);
return deferred.promise;
};
return {
"search": search
};
}]
);
~~~
這個Service主要做的是創建ElasitcSearch Query,然后返回解析結果。
**設計思路**
1. 判斷是否有上次記錄的位置信息,如果有則將地圖的中心設置為上次的位置。
2. 將位置添加到ElasticSearch的Query中。
3. 從ElasticSearch中獲取數據,并解析Render到地圖上。
**OpenLayer**
> OpenLayers是一個用于開發WebGIS客戶端的JavaScript包。OpenLayers 支持的地圖來源包括Google Maps、Yahoo、 Map、微軟Virtual Earth 等,用戶還可以用簡單的圖片地圖作為背景圖,與其他的圖層在OpenLayers 中進行疊加,在這一方面OpenLayers提供了非常多的選擇。除此之外,OpenLayers實現訪問地理空間數據的方法都符合行業標準。OpenLayers 支持Open GIS 協會制定的WMS(Web Mapping Service)和WFS(Web Feature Service)等網絡服務規范,可以通過遠程服務的方式,將以OGC 服務形式發布的地圖數據加載到基于瀏覽器的OpenLayers 客戶端中進行顯示。OpenLayers采用面向對象方式開發,并使用來自Prototype.js和Rico中的一些組件。
### Step 6: Ionic OpenLayer 地圖顯示
1.下載OpenLayer
2.添加到`index.html`:
~~~
<script src="js/ol.js"></script>
~~~
**創建NSService**
新建一個`MapCtrl`,需要用到`ESService`和?`NSService`,NSService是官方示例中的一個函數,提供了一個`getRendererFromQueryString`方法。
~~~
.factory('NSService', function(){
var exampleNS = {};
exampleNS.getRendererFromQueryString = function() {
var obj = {}, queryString = location.search.slice(1),
re = /([^&=]+)=([^&]*)/g, m;
while (m = re.exec(queryString)) {
obj[decodeURIComponent(m[1])] = decodeURIComponent(m[2]);
}
if ('renderers' in obj) {
return obj['renderers'].split(',');
} else if ('renderer' in obj) {
return [obj['renderer']];
} else {
return undefined;
}
};
return {
"exampleNS": exampleNS
};
})
~~~
**創建基本地圖顯示**
這里我們使用的是Bing地圖:
~~~
var view = new ol.View({
center: map_center,
zoom: 4
});
var controls = ol.control.defaults({rotate: false});
var interactions = ol.interaction.defaults({altShiftDragRotate:false, pinchRotate:false});
var map = new ol.Map({
controls: controls,
interactions: interactions,
layers: [
new ol.layer.Tile({
source: new ol.source.BingMaps({
key: 'Ak-dzM4wZjSqTlzveKz5u0d4IQ4bRzVI309GxmkgSVr1ewS6iPSrOvOKhA-CJlm3',
culture: 'zh-CN',
imagerySet: 'Road'
})
})
],
renderer: NSService.exampleNS.getRendererFromQueryString(),
target: 'map',
view: view
});
~~~
一個簡單的地圖如上如示。
**獲取當前位置**
ngCordova有一個插件是`$cordovaGeolocation`,用于獲取當前的位置。代碼如下所示:
~~~
var posOptions = {timeout: 10000, enableHighAccuracy: true};
$cordovaGeolocation
.getCurrentPosition(posOptions)
.then(function (position) {
var pos = new ol.proj.transform([position.coords.longitude, position.coords.latitude], 'EPSG:4326', 'EPSG:3857');
$localstorage.set('position', [position.coords.latitude, position.coords.longitude].toString());
$localstorage.set('map_center', pos);
view.setCenter(pos);
}, function (err) {
console.log(err)
});
~~~
當獲取到位置時,將位置存儲到`localstorage`中。
**獲取結果并顯示**
最后代碼如下所示,獲取解析后的結果,添加icon
~~~
ESService.search("", 0).then(function(results){
var vectorSource = new ol.source.Vector({ });
$.each(results, function(index, result){
var position = result.location.split(",");
var pos = ol.proj.transform([parseFloat(position[1]), parseFloat(position[0])], 'EPSG:4326', 'EPSG:3857');
var iconFeature = new ol.Feature({
geometry: new ol.geom.Point(pos),
name: result.title,
phone: result.phone_number,
distance: result.distance
});
vectorSource.addFeature(iconFeature);
});
var iconStyle = new ol.style.Style({
image: new ol.style.Icon(({
anchor: [0.5, 46],
anchorXUnits: 'fraction',
anchorYUnits: 'pixels',
opacity: 0.75,
src: 'img/icon.png'
}))
});
var vectorLayer = new ol.layer.Vector({
source: vectorSource,
style: iconStyle
});
map.addLayer(vectorLayer);
});
~~~
**添加點擊事件**
在上面的代碼中添加:
~~~
var element = document.getElementById('popup');
var popup = new ol.Overlay({
element: element,
positioning: 'bottom-center',
stopEvent: false
});
map.addOverlay(popup);
map.on('click', function(evt) {
var feature = map.forEachFeatureAtPixel(evt.pixel,
function(feature, layer) {
return feature;
});
if (feature) {
var geometry = feature.getGeometry();
var coord = geometry.getCoordinates();
popup.setPosition(coord);
$(element).popover({
'placement': 'top',
'html': true,
'content': "<h4>商品:" + feature.get('name') + "</h4>" + '' +
'<div class="button icon-left ion-ios-telephone button-calm button-outline">' +
'<a ng-href="tel: {{result.phone_number}}">' + feature.get('phone') + '</a> </div>' +
"<p class='icon-left ion-ios-navigate'> " + feature.get('distance') + "公里</p>"
});
$(element).popover('show');
} else {
$(element).popover('destroy');
}
});
~~~
當用戶點擊時,調用Bootstrap的Popover來顯示信息。

ElasticSearch Map