使用JanusGraph进行图形数据库入门

引文

我写这篇文章的人连关系型数据库管理系统都没有怎么使用过,所以请不要完全相信内容。欢迎指正。

图数据库是什么?

这指的是用于存储图结构(有向图)的专用数据库。最著名的是Neo4j,其他还有Amazon Neptune、ArangoDB等。

图形数据库有什么好处?

由于具备了专门用于从图中提取信息的查询语言(类似于关系型数据库中的SQL),您无需编写算法即可完成。

JanusGraph 是什么?

这是一个在Linux Foundation下开发的开源图形数据库。最初是一个名为TitanGraph的个人项目,但现在已被分叉并继续开发。它于2017年4月发布了0.1版本,2020年3月发布了0.51版本。

提供Apache TinkerPop的图计算框架上。

许可证为Apache 2 License。

JanusGraph的优点

    • バックエンドのデータベース(データの保存先)としてApache Cassandra, Apache HBase,Berkeley DBなどが選べる。ちょっと試してみる分にはBerkeley DBを選ぶのが非常に楽で、がっつり使う目的でスケーラブルな構成にしたい場合はCassandraを使う、というようなことができます。

 

    • クエリ言語のGremlinは他のグラフDB(Neo4j, Amazon Neptune, Azure Cosmos DB)などでも使用できるため、JanusGraphを後々捨てたとしても完全に無駄にはならないかも。

 

    • Dockerイメージ提供あり。まあ、今時のDBでDocker対応していないのも少ないかとは思いますが……。

 

    Windowsでも問題なく動かせます。

先试试看(下载版)

暂时决定试着具体操作一下。

Java虚拟机

由于JanusGraph是用Java编写的,所以需要安装Java 8。如果您没有安装,可以选择通过Oracle或OpenJDK(如AdoptOpenJDK、Amazon Corretto)进行安装。

下载

在官方网站上,提供了两种不同的安装方法:通过zip文件存档和使用Docker镜像。但首先我们将尝试使用zip文件存档进行安装。

从下载页面上下载janusgraph-0.5.1.zip(截至2020年4月的最新版本)。

curl https://github.com/JanusGraph/janusgraph/releases/download/v0.5.1/janusgraph-0.5.1.zip
unzip janusgraph-0.5.1.zip
janusgraph_01.png
janusgraph_02.png

在bin目录中使用上面的内容。对于Windows,使用.bat文件,对于Linux,使用.sh文件。

启动Gremlin控制台

在JanusGraph(或其基础TinkerPop)中,为了进行图数据库服务器的连接和数据交互,以及查询的实验,提供了一个称为Gremlin控制台的控制台环境。

这个环境是用Java的衍生语言Groovy来运行的。但是,即使不熟悉Groovy也可以轻松处理。

在JanusGraph的根目录上运行gremlin.sh(如果是Windows,则运行gremlin.bat以打开控制台。

$ bin/gremlin.sh
> bin\gremlin.bat

如果出现以下”gremlin>”的提示,就表示OK了。

         \,,,/
         (o o)
-----oOOo-(3)-oOOo-----
SLF4J: Class path contains multiple SLF4J bindings.
SLF4J: Found binding in [jar:file:/C:/******/janusgraph-0.5.0/lib/slf4j-log4j12-1.7.12.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: Found binding in [jar:file:/C:/******/janusgraph-0.5.0/lib/logback-classic-1.1.3.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: See http://www.slf4j.org/codes.html#multiple_bindings for an explanation.
SLF4J: Actual binding is of type [org.slf4j.impl.Log4jLoggerFactory]
plugin activated: tinkerpop.server
plugin activated: tinkerpop.tinkergraph
15:55:46 WARN  org.apache.hadoop.util.NativeCodeLoader  - Unable to load native-hadoop library for your platform... using builtin-java classes where applicable
plugin activated: tinkerpop.hadoop
plugin activated: tinkerpop.spark
plugin activated: tinkerpop.utilities
plugin activated: janusgraph.imports
gremlin>

目前图数据库服务器尚未启动,但即使在这种情况下,您也可以尝试各种操作。

请查询官方信息以了解有关Gremlin控制台更详细的使用方法。此外,我还整理了一些豆知识在以下文章中。

Gremlin控制台小技巧

生成图表(用于练习)

生成一个用于练习的嵌入式图,并尝试使用查询语言Gremlin进行查询。
输入以下命令。

gremlin> graph = TinkerFactory.createModern()

回答会贴上如下内容,并会生成相应的图表。

gremlin> graph = TinkerFactory.createModern()
==>tinkergraph[vertices:6 edges:6]

这是一个类似于图表的图示(引用自Apache TinkerPop的文档)。

janusgraph_03.png

这是一个表示开发者和开发语言之间关系的图表。该图表有6个顶点和6条边。每个顶点和边都可以具有一个标签和N个属性的规范。
顶点的标签有”人”和”软件”两种类型,属性有”name”、”age”、”语言”三种类型。
边的标签有”创建”和”认识”两种类型,属性只有一个”权重”。另外,顶点和边上的数字表示ID。

图和遍历源

我們剛剛建立的圖是從JanusGraph(TinkerPop)的Graph類派生出來的。這個類主要用於參考/修改圖的各種特定設定。它也具備編輯圖頂點和邊的數據功能,但我們很少直接操作它們。

关于图形的查询通过名为GraphTraversalSource的遍历(Traversal)接口进行。可以通过以下方式获得查询:

gremlin> g = graph.traversal()
gremlin> g = graph.traversal()
==>graphtraversalsource[tinkergraph[vertices:6 edges:6], standard]

基本上,我们将通过g来进行数据的添加/修改/查询。你可以随意更改g的名称,但一般情况下我们使用此名称。

基础查询

终于,准备工作完成了,我将尝试执行查询。

暂时在这里只详细介绍简单查询的方法,关于更详细的Gremlin操作方法,请参阅TinkerPop文件。全篇都是相当庞大的英文内容,如果不阅读它,实际应用会变得困难。

Apache TinkerPop Getting Started: チュートリアル

TinkerPop Documentation: リファレンス

The Graph: グラフの各要素(頂点/辺/ラベル/プロパティ)について

The Traversal: 各種走査方法についての説明

访问顶点/边/属性

以下是基本操作的七个说明。

走査名説明V([id, …])頂点(Vertex)を選択するE([id, …])辺(Edge)を選択するhas(property_name, value)
has(label_name, property_name, value)特定の値のプロパティを持っている要素(Element)※1を選択するhasLabel(label_name)特定の値のラベルを持っている要素を選択するvalues([property_name, …])要素の持っているプロパティの値を列挙するvalueMap([property_name, …])要素の持っているプロパティの値をMap形式※2で取得するid()要素の持っているIDの値を取得する

※1 在JanusGraph(TinkerPop)中,将Vertex和Edge统称为Element。

※2 有些人喜欢用关联数组、有些人喜欢用字典、有些人喜欢用哈希表等来称呼那个东西。

首先,我们尝试获取所有顶点。

gremlin> g.V()
==>v[1]  // 結果の中身が頂点(Vertex)の場合は v[ID]の形で表示される
==>v[2]
==>v[3]
==>v[4]
==>v[5]
==>v[6]

有6个顶点,并且它们都已被列举出来。V命令用于将图的元素限制为顶点(Vertex)。您也可以通过在括号内指定ID来选择具有该ID的顶点。

gremlin> g.V(2, 4)
==>v[2]
==>v[4]

在[]中,显示了用于识别顶点的ID,但这样并不直观。接下来,我们尝试从顶点的属性中获取名称(name)。

gremlin> g.V().values("name")
==>marko
==>vadas
==>lop
==>josh
==>ripple
==>peter

通过Gremlin查询,我们可以通过不断在方法链中添加内容来筛选数据和传播影响。values可以看作是SQL中的SELECT,当values的参数为空时,将会返回所有属性。

gremlin> g.V().values()
==>marko
==>29
==>vadas
==>27
==>lop
==>java
==>josh
==>32
==>ripple
==>java
==>peter
==>35

也许返回的结果没有结构化,可能很难使用。还有一个名为valueMap的选项。这个选项可以将所有数据整理到一个哈希图(字典)中。用法与前一个选项相同,只是返回结果的方式不同。哪个更好取决于目的。

gremlin> g.V().valueMap()
==>[name:[marko],age:[29]]
==>[name:[vadas],age:[27]]
==>[name:[lop],lang:[java]]
==>[name:[josh],age:[32]]
==>[name:[ripple],lang:[java]]
==>[name:[peter],age:[35]]

根据属性信息来筛选元素(顶点或边),可使用has函数。

gremlin> g.V().has("name", "josh")  //nameがjoshである頂点を探す
==>v[4]
gremlin> g.V().has("name", "josh").valueMap()
==>[name:[josh],age:[32]]

要通过标签进行筛选,请使用hasLabel。

gremlin> g.V().hasLabel("person").valueMap("name", "age")
==>[name:[marko],age:[29]]
==>[name:[vadas],age:[27]]
==>[name:[josh],age:[32]]
==>[name:[peter],age:[35]]

如果您想要在标签和属性两个方面同时筛选,可以使用三个参数的has方法。

gremlin> g.V().has("person", "name", "josh").valueMap("name", "age")
==>[name:[josh],age:[32]]

在这里我们只解释了字符串的匹配,但是has方法还可以根据不同的条件进行过滤,例如字符串的部分匹配或者数字的大小比较等。详细信息请参阅参考资料中的Has-Step部分。

顺便说一下,你可能会觉得valueMap的值被方括号包围着有点奇怪。因为顶点和边可以拥有多个相同名称的属性,所以返回列表是理所当然的。但是,如果只假设只有一个属性,你可以通过以下方式去除括号(关于by和unfold的解释在这里不进行)。

gremlin> g.V().has("name", "josh").valueMap().by(__.unfold())
==>[name:josh,age:32]

注意:在gremlin控制台中,可以省略__.(在后面会解释,在Python中是必需的)。

接下来,我们尝试获取ID的值。这非常简单,我们使用id即可。

gremlin> g.V().has("name", "marko").id()
==>1

接下来,我们将类似地确定边缘(Edge)的筛选方式。对于边缘,我们使用符号E。

gremlin> g.E()
==>e[7][1-knows->2]
==>e[8][1-knows->4]
==>e[9][1-created->3]
==>e[10][4-created->5]
==>e[11][4-created->3]
==>e[12][6-created->3]

虽然比顶点情况下有更多的信息,但这是因为gremlin控制台自动显示出来的,实际上需要自己手动逐个检索。

在边的情况下,您也可以像顶点一样使用has、hasLabel、values等方法。

gremlin> g.E().has("weight", 1.0)
==>e[8][1-knows->4]
==>e[10][4-created->5]
gremlin> g.E().hasLabel("created")
==>e[9][1-created->3]
==>e[10][4-created->5]
==>e[11][4-created->3]
==>e[12][6-created->3]
gremlin> g.E().values()
==>0.5
==>1.0
==>0.4
==>1.0
==>0.4
==>0.2

简单遍历顶点和边

我想你已经理解了访问信息的基本操作,接下来我将解释如何遍历由顶点和边构成的图。

根据查询目标是顶点还是边,可以进行不同类型的遍历。

对于顶点的遍历

走査名説明結果の種類outE()頂点から出ている辺の取得Edgeout()頂点から出ている辺の先の頂点の取得VertexinE()頂点に入ってくる辺の取得Edgein()頂点に入ってくる辺の元の頂点の取得VertexbothE()頂点に対してつながっている辺の取得。方向不問Edgeboth()頂点に対してつながっている頂点の取得。方向不問Vertex

检索周围

走査名説明結果の種類outV()辺につながっている元の頂点の取得VertexinV()辺につながっている先の頂点の取得Vertex

如果需要边的信息,您可以使用inE、outE、inV、outV和bothE函数来交替遍历顶点和边。相反,如果不需要边的信息(仅需查看顶点之间的连接),则可以使用out、in和both函数来遍历顶点。

gremlin> g.V(1).outE()  // 頂点1から出ている辺
==>e[9][1-created->3]
==>e[7][1-knows->2]
==>e[8][1-knows->4]
gremlin> g.V(1).outE().outV()  // 来た方向(頂点1)に戻る(無意味)
==>v[1]
==>v[1]
==>v[1]
gremlin> g.V(1).outE().inV() // 頂点1の先にある頂点を選ぶ
==>v[3]
==>v[2]
==>v[4]
gremlin> g.V(1).out()  // outは outE&inVとイコール
==>v[3]
==>v[2]
==>v[4]
gremlin> g.V(4).both()
==>v[5]
==>v[3]
==>v[1]

只要记住outE表示出边和入顶点,inE表示入边和出顶点,就可以轻松处理,而不会感到困惑。

获取路径

使用路径可以按顺序输出先前遍历的路径。

gremlin> g.V(1).out().out().path()  // 頂点1(marko)から2つ先に進める経路
==>[v[1],v[4],v[5]]  // 順にv(1)の結果、out()の結果、out()の結果
==>[v[1],v[4],v[3]]
gremlin> g.V(1).out().out().values("name").path()  // 順に出力されるのは頂点や辺に限らず、プロパティ等も例外ではない
==>[v[1],v[4],v[5],ripple] // 順にv(1),out(),out(),values()の結果
==>[v[1],v[4],v[3],lop]
gremlin> g.V(1).out().out().path().by("name")  // 頂点が返ってくるところを特定のプロパティに置き換えたい場合はbyを用いる
==>[marko,josh,ripple]
==>[marko,josh,lop]

为了获取第二个顶点,我们可以使用out().out()之类的写法,但是要获取第十个顶点,使用同样的方式太土了。在这种情况下,我们可以使用repeat和times。

gremlin> g.V(1).repeat(__.out()).times(2).path()
==>[v[1],v[4],v[5]]
==>[v[1],v[4],v[3]]

以前我们已经提到过,这个__是一个特殊的命名空间,它包含了与遍历名称相同的各种函数。当传递给遍历的返回值时,可以执行与该名称相对应的处理。例如,像上面的例子一样,将__.out()传递给repeat遍历将会重复执行out。而将__.in()传递进去则会反向重复执行。在TinkerPop中,这被称为匿名遍历(参考)。如果用设计模式来解释,可以说这是策略模式。

添加数据 (顶点/边/属性)

在 Gremlin 中,自然可以添加数据。

走査名説明addV(label_name)頂点(Vertex)を追加するaddE(label_name)辺(Edge)を追加するproperty(property_name, value)プロパティを追加・更新するdrop()要素またはプロパティを削除する

如果要添加一个顶点,可以使用addV命令。

gremlin> g.addV("person")
==>v[13]
gremlin> g.V()
==>v[1]
==>v[2]
==>v[3]
==>v[4]
==>v[5]
==>v[6]
==>v[13]  //追加されている

要添加边,可以使用addE函数,其中需要指定from和to参数。

g.addE("knows").from(g.V(1)).to(g.V(13))
==>e[14][1-knows->13]

在边上,必须指定顶点作为其两端。
在”from”中指定顶点作为边的起点,在”to”中指定顶点作为边的终点。

要设置属性,请使用 property。

gremlin> g.V(13).values()
gremlin> g.V(13).property("name", "bob").property("age", 33)
==>v[13]
gremlin> g.V(13).values()
==>bob
==>33

如果顶点、边或属性不再需要,可以使用”drop”命令进行删除。

gremlin> g.V(13).properties("name").drop()
gremlin> g.V(13).values()
==>33
gremlin> g.V(13).drop()
gremlin> g.V()
==>v[1]
==>v[2]
==>v[3]
==>v[4]
==>v[5]
==>v[6]

properties没有在这里进行介绍,它是用于从元素中提取属性的命令。对于属性,可以执行key(提取属性名)、value(提取值)或drop(删除)等命令。

有关更高级查询的(小问答集)

在这里提到的功能只是Gremlin的一小部分,并且提供了执行更复杂和精细查询的方法,但由于这可能需要写几篇文章的内容量,因此请每个人参考相关文献。

我会根据我目前了解到的信息,在这里给出一个链接集,告诉你在这种情况下应该使用哪些信息。官方的「Recipes」中有很多内容呢。

    • Q.ループを検出するには?

A. 公式の「Cycle Detection」を参照

Q.頂点や辺の重複を見つけて削除するには?

A. 公式の「Duplicate Vertex Detection」や「Duplicate Edge Detection」を参照

Q.頂点と頂点の最短経路を取得するには?

A. 公式の「Shortest Path」またはstackoverflowの「Best way to find a shortest path between two nodes in Tinkerpop 3.1」

Q.SQLなら分かるんだけど、そういう人向けのGremlin解説はないの?

A. なんかそれっぽいのはあります(よく読んでませんが) => 「SQL2Gremlin」

安装JanusGraph数据库

虽然单独使用控制台可以创建和操作图形(遍历),但数据不会被保存(持久化)。一旦关闭控制台,所做的工作内容将会丢失。在这里,我将解释一下如何运行一个名为JanusGraph的Gremlin服务器。

使用伯克利数据库启动服务器。

在JanusGraph中,可以选择将数据存储在Apache Cassandra,Apache HBase,Berkeley DB或不使用(内存中)。此外,可以选择使用Lucene,ElasticSearch或Solr作为索引处理(索引后端)。默认情况下,使用ElasticSearch,但如果不需要索引,则不需要采取任何特殊行动。在本次尝试中,将使用相对简单的Berkeley DB和ElasticSearch配置进行启动(不使用索引)。

JanusGraph的档案中的conf/gremlin-server目录中包含了默认的各种配置文件。

設定ファイル名ストレージ・バックエンド(括弧内は接続先)インデックス・バックエンド(括弧内は接続先)gremlin-server.yamlApache Cassandra
(127.0.0.1)Elastic Search
(127.0.0.1)gremlin-server-berkeleyje.yamlBerkekey DB
(127.0.0.1)Elastic Search
(127.0.0.1)gremlin-server-berkeleyje-es.yamlBerkekey DB
(127.0.0.1)Elastic Search
(elasticsearch)gremlin-server-configuration.yamlApache Cassandra
(127.0.0.1)Elastic Search
(127.0.0.1)

正直说,设定文件的数量和变化很微弱。如果您想选择其他配置,就需要自己准备(只需复制粘贴设定文件并修改graphs部分,大致上就能完成)。

本次使用 gremlin-server-berkeleyje.yaml 文件。从根目录下执行 bin/gremlin-server.sh 或者 bin\gremlin-server.bat(Windows)。

$ bin/gremlin-server.sh conf/gremlin-server/gremlin-server-berkeleyje.yaml
> bin\gremlin-server.bat conf\gremlin-server\gremlin-server-berkeleyje.yaml

只要最后显示以下类似的日志,就可以流出大量日志。

4839 [gremlin-server-boss-1] INFO  org.apache.tinkerpop.gremlin.server.GremlinServer  - Channel started at port 8182.

如果要停止服务器,请输入Ctrl+C。

通过Gremlin控制台进行远程连接。

当服务器启动后,将访问服务器上的图表。您可以使用为各种编程语言准备的驱动程序进行连接,但首先请尝试使用之前使用的Gremlin控制台进行连接。

在Gremlin控制台上,使用:remote命令与服务器进行连接。

gremlin> :remote connect tinkerpop.server conf/remote.yaml

只要以下回答出现,就算成功了。

==>Configured localhost/127.0.0.1:8182

这样一来,虽然仍然在同一台电脑上,但已经变成了远程连接状态,因此以后可以使用”>”命令将指令发送到服务器。”>”是”submit”命令的缩写形式,用于将指令脚本传输到服务器的命令。

在此次服务器设置中,已经预先准备了图形变量作为graph和GraphTraversalSource变量,以图表形式表示。

gremlin> :> graph
==>standardjanusgraph[berkeleyje:db/berkeley]
gremlin> :> g
==>graphtraversalsource[standardjanusgraph[berkeleyje:db/berkeley], standard]

请注意本地变量和远程变量完全独立,避免混淆和困惑。

一开始图表是空的,所以我随便添加一些数据来试试。

gremlin> :> g.addV("person").property("name", "marko")
==>v[4288]
gremlin> :> g.addV("person").property("name", "bob")
==>v[40964224]
gremlin> :> g.V()
==>v[40964224]
==>v[4288]

頂点的ID会被随机设置,但这取决于图的配置和实现方式。在JanusGraph的默认配置中,似乎是随机的。

如果要解除远程连接:请使用”remote close”命令。即使解除连接,额外添加的数据将保留下来。

通过程序连接(Python的情况)

由于数据持久化的能力已经实现,所以现在只需要编写可以利用数据库的程序。针对各种编程语言已经准备了对应的驱动程序,我们将使用它们。有关支持的语言,请参考列表。针对Java、Groovy、Python、.NET和Javascript,我们已经提供了官方说明。

本次我将尝试使用Python。Python的Gremlin库是gremlinpython (PyPI)。

可以使用以下命令从pip进行安装。

$ python -m pip install gremlinpython

以下是发送简单查询的程序。

from gremlin_python.driver.driver_remote_connection import DriverRemoteConnection
from gremlin_python.process.anonymous_traversal import traversal
from gremlin_python.process.graph_traversal import __
from gremlin_python.process.traversal import T, P, TextP

domain = "localhost"  # GremlinサーバーのIPアドレスorドメイン名
port = 8182  # Gremlinサーバーのポート番号(デフォルトでは8182)
traversal_name = "g"  # TraversalGraphSourceの変数名(デフォルトでg)
url = f"ws://{domain}:{port}/gremlin"  # 接続するURL

# サーバーと接続する
connection = DriverRemoteConnection(url, traversal_name)   # ①
g = traversal().withRemote(connection)   # ②

# クエリを発行
print("Query: toList version")
vertices = g.V().values("name").toList()  # ③ toListを使う場合
for vertex in vertices:
    print(vertex)

print("Query: next version")
vertices = g.V().values("name")  # ③ hasNext,nextを使う場合
while vertices.hasNext():
    print(vertices.next())

# サーバーとの接続を解除
connection.close()  # ④

连接到Gremlin服务器,并指定URL和要访问的图表。URL的格式为ws://host:port/gremlin。默认情况下,URL是ws://127.0.0.1:8182/gremlin。要访问的图表由GraphTraversalSource的名称指定。在这种情况下,图表名称是g。

从已建立的连接中获取用于发送查询的g接口。这个接口的使用方法与Gremlin控制台几乎一样,可以使用。

③ 发出查询。这是一个简单的查询语句g.V().values(“name”)。需要注意的是,在最后必须添加一个称为Terminal-Steps的指令。如果不添加这个指令,查询将不会执行(Gremlin控制台很贴心地自动添加了它)。Terminal-Steps包括hasNext、next、toList、iterate等,关于使用哪个的选择标准,我认为可以考虑以下几点。

    • 結果の数が少ないとわかっている場合 → toListを使う。結果をリストで得ることができます。

 

    • 結果の数が単数、あるいは不明の場合 → hasNext,nextを使う。hasNextは次のデータがあるか判定し、nextはデータを一つ取り出せます。この2つを組み合わせて、whileループによるイテレーションが可能です。

 

    データを追加/削除するクエリの場合 → iterateを使う。

完成使用后,请取消连接。

通过Docker轻松安装数据库

如果对Docker没有兴趣的人可以不必阅读。

尽管之前我们是从下载的JanusGraph存档中启动服务器,但由于官方提供了Docker镜像,我们也可以尝试在这里启动服务器。如果服务器已经在运行,请先停止它。此外,请确保已经单独安装了Docker。

在公式安装页面上有适用于Docker的命令,但稍微不太完美,我将介绍稍作调整的版本。

docker run --name my_janus -d -p 8182:8182 -v janusgraph-default-data:/var/lib/janusgraph janusgraph/janusgraph:latest

执行上述命令后,将启动一个使用Berkeley DB配置的Gremlin服务器(但索引后端将是Lucene)。

与公式命令的差异有两个,一个是给容器取了一个名字(my_janus)。因为如果没有给容器命名,以后的处理会变得麻烦。另一个是指定了卷。如果使用公式命令删除容器,数据库的数据也会一起消失,所以有点不方便。通过指定卷,即使删除容器,数据也能保留下来。

在中文中,可以用以下方式重新表达这句话:

停止容器可以使用命令 docker stop my_janus,重新启动可以使用命令 docker start my_janus。

许多人认为,在组合多个容器时,不仅仅使用Docker本身,还可以使用Docker Compose。在这种情况下,使用官方提供的docker-compose.yml作为基础配置将是一个不错的选择。

请参考此处的设置,使用Docker镜像以便使用不同的存储后端。我尚未尝试过。

在中文中,”終わりに”可以翻译为 “最后”。

这只是一个开始,要真正运用起来,需要了解很多东西,比如缓存、索引、架构、事务、部署、备份等等。我也正在学习中。如果我有什么了解,我会写新的文章。

bannerAds