Skip to content

Commit e559f9c

Browse files
committed
Add search_tags_count.
1 parent db4f589 commit e559f9c

File tree

7 files changed

+327
-0
lines changed

7 files changed

+327
-0
lines changed

function02-template.yaml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,23 @@ Resources:
311311
Timeout: 15
312312
TracingConfig:
313313
Mode: "Active"
314+
SearchTagsCount:
315+
Type: "AWS::Lambda::Function"
316+
Properties:
317+
Code: ./deploy/articles_tip_ranking.zip
318+
Environment:
319+
Variables:
320+
ELASTIC_SEARCH_ENDPOINT: !Ref ElasticSearchEndpoint
321+
Handler: handler.lambda_handler
322+
MemorySize: 3008
323+
Role:
324+
Fn::ImportValue:
325+
Fn::Sub: "${AlisAppId}-LambdaRole"
326+
Runtime: python3.6
327+
Timeout: 300
328+
TracingConfig:
329+
Mode: "Active"
330+
314331

315332
Outputs:
316333
ArticleSupportersIndex:
@@ -373,3 +390,7 @@ Outputs:
373390
Value: !GetAtt TopicsCryptoRankingIndex.Arn
374391
Export:
375392
Name: !Sub "${AlisAppId}-TopicsCryptoRankingIndex"
393+
SearchTagsCount:
394+
Value: !GetAtt SearchTagsCount.Arn
395+
Export:
396+
Name: !Sub "${AlisAppId}-SearchTagsCount"

src/common/es_util.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# -*- coding: utf-8 -*-
22
import settings
33
import os
4+
import time
45

56

67
class ESUtil:
@@ -38,6 +39,35 @@ def search_tag(elasticsearch, word, limit, page):
3839

3940
return tags
4041

42+
@staticmethod
43+
def search_tags_count(elasticsearch, size=500, term=604800):
44+
from_time = round(time.time()) - term
45+
body = {
46+
'size': 0,
47+
'query': {
48+
'bool': {
49+
'must': [
50+
{'range': {'published_at': {'gte': from_time}}}
51+
]
52+
}
53+
},
54+
'aggs': {
55+
'tags_count': {
56+
'terms': {
57+
'field': 'tags.keyword',
58+
'order': {'_count': 'desc'},
59+
'size': size
60+
}
61+
}
62+
}
63+
}
64+
65+
response = elasticsearch.search(
66+
index='articles',
67+
body=body
68+
)
69+
return response['aggregations']['tags_count']['buckets']
70+
4171
@staticmethod
4272
def search_article(elasticsearch, limit, page, word=None, tag=None):
4373
body = {

src/common/settings.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,15 @@
150150
},
151151
'maxItems': 5
152152
},
153+
'tags_count': {
154+
'type': 'array',
155+
'items': {
156+
'type': 'string',
157+
'minLength': 1,
158+
'maxLength': 25
159+
},
160+
'maxItems': 150
161+
},
153162
'tip_value': {
154163
'type': 'number',
155164
'minimum': 1,
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import os
2+
from search_tags_count import SearchTagsCount
3+
from elasticsearch import Elasticsearch, RequestsHttpConnection
4+
from requests_aws4auth import AWS4Auth
5+
6+
awsauth = AWS4Auth(
7+
os.environ['AWS_ACCESS_KEY_ID'],
8+
os.environ['AWS_SECRET_ACCESS_KEY'],
9+
os.environ['AWS_REGION'],
10+
'es',
11+
session_token=os.environ['AWS_SESSION_TOKEN']
12+
)
13+
elasticsearch = Elasticsearch(
14+
hosts=[{'host': os.environ['ELASTIC_SEARCH_ENDPOINT'], 'port': 443}],
15+
http_auth=awsauth,
16+
use_ssl=True,
17+
verify_certs=True,
18+
connection_class=RequestsHttpConnection
19+
)
20+
21+
22+
def lambda_handler(event, context):
23+
search_tags_count = SearchTagsCount(event, context, elasticsearch=elasticsearch)
24+
return search_tags_count.main()
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import json
2+
3+
from jsonschema import validate
4+
5+
import settings
6+
from decimal_encoder import DecimalEncoder
7+
from es_util import ESUtil
8+
from lambda_base import LambdaBase
9+
10+
11+
class SearchTagsCount(LambdaBase):
12+
def get_schema(self):
13+
return {
14+
'type': 'object',
15+
'properties': {
16+
'tags': settings.parameters['tags_count']
17+
},
18+
'required': ['tags']
19+
}
20+
21+
def validate_params(self):
22+
validate(self.params, self.get_schema())
23+
24+
def exec_main_proc(self):
25+
# 直近1週間分のタグを集計
26+
search_size = len(self.params['tags']) * settings.parameters['tags']['maxItems']
27+
from_time = 86400 * 7
28+
search_result = ESUtil.search_tags_count(self.elasticsearch, search_size, from_time)
29+
30+
# 集計結果より指定タグの件数を取得
31+
temp = []
32+
for tag in self.params['tags']:
33+
tag_count = [d['doc_count'] for d in search_result if d['key'] == tag]
34+
temp.append({
35+
'tag': tag,
36+
'count': tag_count[0] if len(tag_count) > 0 else 0
37+
})
38+
result = sorted(temp, key=lambda x: x['count'], reverse=True)
39+
return {
40+
'statusCode': 200,
41+
'body': json.dumps(result, cls=DecimalEncoder)
42+
}

swagger/swagger.yaml

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,13 @@ definitions:
256256
type: integer
257257
created_at:
258258
type: integer
259+
TagCount:
260+
type: object
261+
properties:
262+
tag:
263+
type: string
264+
count:
265+
type: integer
259266
Fraud:
260267
type: object
261268
properties:
@@ -498,6 +505,38 @@ paths:
498505
passthroughBehavior: when_no_templates
499506
httpMethod: POST
500507
type: aws_proxy
508+
/search/tags_count:
509+
get:
510+
description: "タグ件数検索"
511+
parameters:
512+
- name: "tags"
513+
in: "query"
514+
description: "検索タグ"
515+
required: true
516+
type: array
517+
items:
518+
type: string
519+
responses:
520+
"200":
521+
description: "タグ件数一覧"
522+
schema:
523+
type: array
524+
items:
525+
$ref: '#/definitions/TagCount'
526+
x-amazon-apigateway-integration:
527+
responses:
528+
default:
529+
statusCode: "200"
530+
uri:
531+
Fn::Join:
532+
- ''
533+
- - Fn::Sub: "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/"
534+
- Fn::ImportValue:
535+
Fn::Sub: "${AlisAppId}-SearchTagsCount"
536+
- "/invocations"
537+
passthroughBehavior: when_no_templates
538+
httpMethod: POST
539+
type: aws_proxy
501540
/articles/recent:
502541
get:
503542
description: "最新記事一覧情報を取得"
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
import json
2+
from unittest import TestCase
3+
from search_tags_count import SearchTagsCount
4+
from elasticsearch import Elasticsearch
5+
from unittest.mock import patch, MagicMock
6+
7+
8+
class TestSearchTagsCount(TestCase):
9+
elasticsearch = Elasticsearch(
10+
hosts=[{'host': 'localhost'}]
11+
)
12+
13+
def setUp(self):
14+
items = [
15+
{
16+
'article_id': 'test1',
17+
'created_at': 1530112710,
18+
'title': 'abc1',
19+
'published_at': 1530112710,
20+
'sort_key': 1530112710000000,
21+
'body': 'huga test',
22+
'tags': ['A', 'B', 'C']
23+
},
24+
{
25+
'article_id': 'test2',
26+
'created_at': 1530112720,
27+
'title': 'abc2',
28+
'published_at': 1530112710 + 1,
29+
'sort_key': 1530112720000000,
30+
'body': 'foo bar',
31+
'tags': ['A', 'B']
32+
},
33+
{
34+
'article_id': 'test3',
35+
'created_at': 1530112730,
36+
'title': 'abc3',
37+
'published_at': 1530112710 + 2,
38+
'sort_key': 1530112730000000,
39+
'body': 'foo bar',
40+
'tags': ['A']
41+
},
42+
{
43+
'article_id': 'test4',
44+
'created_at': 1530112740,
45+
'title': 'abc4',
46+
'published_at': 1530112710 - 1,
47+
'sort_key': 1530112700000000,
48+
'body': 'foo bar',
49+
'tags': ['A', 'B', 'C']
50+
},
51+
{
52+
'article_id': 'test5',
53+
'created_at': 1530112750,
54+
'title': 'abc5',
55+
'published_at': 1530112710,
56+
'sort_key': 1530112700000000,
57+
'body': 'foo bar',
58+
'tags': ['ハンカク', '&$%!”#', '𪚲🍣𪚲', 'aaa-aaa', 'abcde vwxyz']
59+
},
60+
]
61+
for item in items:
62+
self.elasticsearch.index(
63+
index="articles",
64+
doc_type="article",
65+
id=item["article_id"],
66+
body=item
67+
)
68+
self.elasticsearch.indices.refresh(index="articles")
69+
70+
def tearDown(self):
71+
self.elasticsearch.indices.delete(index="articles", ignore=[404])
72+
73+
@patch('time.time', MagicMock(return_value=1530112710 + 86400 * 7))
74+
def test_search_request(self):
75+
params = {
76+
'queryStringParameters': {
77+
'tags': ['A', 'B', 'C', 'D']
78+
}
79+
}
80+
response = SearchTagsCount(params, {}, elasticsearch=self.elasticsearch).main()
81+
actual = json.loads(response['body'])
82+
expected = [
83+
{'count': 3, 'tag': 'A'},
84+
{'count': 2, 'tag': 'B'},
85+
{'count': 1, 'tag': 'C'},
86+
{'count': 0, 'tag': 'D'}
87+
]
88+
self.assertEqual(response['statusCode'], 200)
89+
self.assertEqual(expected, actual)
90+
91+
def test_search_request_not_exists(self):
92+
params = {
93+
'queryStringParameters': {
94+
'tags': ['あ', 'い', 'う']
95+
}
96+
}
97+
response = SearchTagsCount(params, {}, elasticsearch=self.elasticsearch).main()
98+
actual = json.loads(response['body'])
99+
expected = [
100+
{'count': 0, 'tag': 'あ'},
101+
{'count': 0, 'tag': 'い'},
102+
{'count': 0, 'tag': 'う'}
103+
]
104+
self.assertEqual(response['statusCode'], 200)
105+
self.assertEqual(expected, actual)
106+
107+
@patch('time.time', MagicMock(return_value=1530112710 + 86400 * 7))
108+
def test_search_with_tag_half_kana(self):
109+
params = {
110+
'queryStringParameters': {
111+
'tags': ['ハンカク', '&$%!”#', '𪚲🍣𪚲', 'aaa-aaa', 'abcde vwxyz']
112+
}
113+
}
114+
response = SearchTagsCount(params, {}, elasticsearch=self.elasticsearch).main()
115+
actual = json.loads(response['body'])
116+
expected = [
117+
{'count': 1, 'tag': 'ハンカク'},
118+
{'count': 1, 'tag': '&$%!”#'},
119+
{'count': 1, 'tag': '𪚲🍣𪚲'},
120+
{'count': 1, 'tag': 'aaa-aaa'},
121+
{'count': 1, 'tag': 'abcde vwxyz'}
122+
]
123+
self.assertEqual(response['statusCode'], 200)
124+
self.assertEqual(expected, actual)
125+
126+
def test_search_request_tags_over150(self):
127+
# tags 数150超
128+
temp_tags = []
129+
for i in range(151):
130+
temp_tags.append('AAA')
131+
132+
params = {
133+
'queryStringParameters': {
134+
'tags': temp_tags,
135+
}
136+
}
137+
response = SearchTagsCount(params, {}, elasticsearch=self.elasticsearch).main()
138+
self.assertEqual(response['statusCode'], 400)
139+
140+
def test_search_no_params(self):
141+
params = {
142+
'queryStringParameters': {}
143+
}
144+
response = SearchTagsCount(params, {}, elasticsearch=self.elasticsearch).main()
145+
self.assertEqual(response['statusCode'], 400)
146+
147+
def test_invalid_tag_parmas(self):
148+
params = {
149+
'queryStringParameters': {
150+
'tags': ['']
151+
}
152+
}
153+
response = SearchTagsCount(params, {}, elasticsearch=self.elasticsearch).main()
154+
self.assertEqual(response['statusCode'], 400)
155+
156+
params = {
157+
'queryStringParameters': {
158+
'tags': ['A' * 26]
159+
}
160+
}
161+
response = SearchTagsCount(params, {}, elasticsearch=self.elasticsearch).main()
162+
self.assertEqual(response['statusCode'], 400)

0 commit comments

Comments
 (0)